create-byan-agent 2.15.0 → 2.17.0
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/CHANGELOG.md +78 -0
- package/README.md +24 -0
- package/install/GUIDE-INSTALLATION-BYAN-SIMPLE.md +40 -0
- package/install/bin/create-byan-agent-v2.js +9 -0
- package/install/lib/claude-native-setup.js +37 -0
- package/install/lib/mcp-extensions/gdrive.js +256 -0
- package/install/lib/mcp-extensions/index.js +147 -0
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/mcp-config.js +107 -8
- package/install/packages/platform-config/lib/validate.js +0 -14
- package/install/src/webui/api.js +6 -0
- package/install/src/webui/server.js +8 -1
- package/install/templates/.claude/CLAUDE.md +18 -0
- package/install/templates/.claude/hooks/lib/strict-config.json +46 -0
- package/install/templates/.claude/hooks/lib/strict-runtime.js +82 -0
- package/install/templates/.claude/hooks/strict-context-inject.js +86 -0
- package/install/templates/.claude/hooks/strict-scope-guard.js +101 -0
- package/install/templates/.claude/hooks/strict-stop-guard.js +100 -0
- package/install/templates/.claude/rules/strict-mode.md +166 -0
- package/install/templates/.claude/settings.json +12 -0
- package/install/templates/.claude/skills/byan-strict/SKILL.md +54 -0
- package/install/templates/.githooks/pre-commit +15 -0
- package/install/templates/_byan/_config/strict-mode.yaml +258 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-sync-rules.js +24 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/strict-precommit-gate.js +21 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +2 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +120 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-activation.js +76 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +391 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-sync.js +140 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +261 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +207 -1
- package/package.json +6 -2
- package/src/byan-v2/data/strict-mantras.json +188 -0
- package/src/byan-v2/generation/mantra-validator.js +39 -4
- package/update-byan-agent/__tests__/migrate-mcp-config.test.js +74 -24
- package/update-byan-agent/lib/migrate-mcp-config.js +33 -27
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"source": "byan-sync-rules",
|
|
4
|
+
"generated_from": "_byan/_config/strict-mode.yaml",
|
|
5
|
+
"count": 12
|
|
6
|
+
},
|
|
7
|
+
"mantras": [
|
|
8
|
+
{
|
|
9
|
+
"id": "STRICT-1",
|
|
10
|
+
"title": "Scope Lock First",
|
|
11
|
+
"description": "Reformulate the request and lock the scope before any build. The locked scope is the contract for the rest of the task.",
|
|
12
|
+
"validation": {
|
|
13
|
+
"type": "keyword",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"lock the scope",
|
|
16
|
+
"byan_strict_lock_scope",
|
|
17
|
+
"scope lock"
|
|
18
|
+
],
|
|
19
|
+
"required": true
|
|
20
|
+
},
|
|
21
|
+
"priority": "critical"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "STRICT-2",
|
|
25
|
+
"title": "No Downgrade",
|
|
26
|
+
"description": "Deliver the scope that was asked. Do not substitute an MVP, a stub, or a \"simplified version\" unless the user approves it and it is recorded in the audit trail.",
|
|
27
|
+
"validation": {
|
|
28
|
+
"type": "keyword",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"no downgrade",
|
|
31
|
+
"downgrade",
|
|
32
|
+
"mvp"
|
|
33
|
+
],
|
|
34
|
+
"required": true
|
|
35
|
+
},
|
|
36
|
+
"priority": "critical"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "STRICT-3",
|
|
40
|
+
"title": "No Silent Cut",
|
|
41
|
+
"description": "If a part of the scope cannot be done, surface it as a gap in the self_verify findings. Do not drop it quietly.",
|
|
42
|
+
"validation": {
|
|
43
|
+
"type": "keyword",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"silent cut",
|
|
46
|
+
"surface it as a gap",
|
|
47
|
+
"do not cut silently"
|
|
48
|
+
],
|
|
49
|
+
"required": true
|
|
50
|
+
},
|
|
51
|
+
"priority": "high"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "STRICT-4",
|
|
55
|
+
"title": "Self-Verify Three Times",
|
|
56
|
+
"description": "Run at least three self-verification passes against the locked acceptance criteria before calling complete.",
|
|
57
|
+
"validation": {
|
|
58
|
+
"type": "keyword",
|
|
59
|
+
"keywords": [
|
|
60
|
+
"self-verify",
|
|
61
|
+
"byan_strict_self_verify",
|
|
62
|
+
"three times"
|
|
63
|
+
],
|
|
64
|
+
"required": true
|
|
65
|
+
},
|
|
66
|
+
"priority": "critical"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "STRICT-5",
|
|
70
|
+
"title": "Re-Read The Original",
|
|
71
|
+
"description": "Each self-verify pass re-reads the original request, not your own summary of it.",
|
|
72
|
+
"validation": {
|
|
73
|
+
"type": "keyword",
|
|
74
|
+
"keywords": [
|
|
75
|
+
"re-read",
|
|
76
|
+
"original request"
|
|
77
|
+
],
|
|
78
|
+
"required": true
|
|
79
|
+
},
|
|
80
|
+
"priority": "high"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "STRICT-6",
|
|
84
|
+
"title": "Evidence Over Assertion",
|
|
85
|
+
"description": "A \"done\" claim needs an artifact (test output, file diff, command result), not a statement.",
|
|
86
|
+
"validation": {
|
|
87
|
+
"type": "keyword",
|
|
88
|
+
"keywords": [
|
|
89
|
+
"evidence",
|
|
90
|
+
"artifact",
|
|
91
|
+
"test output"
|
|
92
|
+
],
|
|
93
|
+
"required": true
|
|
94
|
+
},
|
|
95
|
+
"priority": "high"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "STRICT-7",
|
|
99
|
+
"title": "Hard Claim Floor",
|
|
100
|
+
"description": "Claims in security, performance, or compliance need LEVEL-1 sourcing (95%) or they are BLOCKED.",
|
|
101
|
+
"validation": {
|
|
102
|
+
"type": "keyword",
|
|
103
|
+
"keywords": [
|
|
104
|
+
"level-1",
|
|
105
|
+
"95%",
|
|
106
|
+
"blocked"
|
|
107
|
+
],
|
|
108
|
+
"required": true
|
|
109
|
+
},
|
|
110
|
+
"priority": "critical"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "STRICT-8",
|
|
114
|
+
"title": "Gap Is Not Failure",
|
|
115
|
+
"description": "Reporting a gap is the correct behavior. Hiding a gap is the violation.",
|
|
116
|
+
"validation": {
|
|
117
|
+
"type": "keyword",
|
|
118
|
+
"keywords": [
|
|
119
|
+
"gap is",
|
|
120
|
+
"hiding a gap",
|
|
121
|
+
"the violation"
|
|
122
|
+
],
|
|
123
|
+
"required": true
|
|
124
|
+
},
|
|
125
|
+
"priority": "high"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "STRICT-9",
|
|
129
|
+
"title": "Audit Trail",
|
|
130
|
+
"description": "Every scope lock, verify pass, and completion is appended to the audit log. The commit gate reads this trail.",
|
|
131
|
+
"validation": {
|
|
132
|
+
"type": "keyword",
|
|
133
|
+
"keywords": [
|
|
134
|
+
"audit trail",
|
|
135
|
+
"audit log",
|
|
136
|
+
"audit token"
|
|
137
|
+
],
|
|
138
|
+
"required": true
|
|
139
|
+
},
|
|
140
|
+
"priority": "high"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "STRICT-10",
|
|
144
|
+
"title": "No Premature Complete",
|
|
145
|
+
"description": "complete is rejected below three passes or when the last pass found a gap.",
|
|
146
|
+
"validation": {
|
|
147
|
+
"type": "keyword",
|
|
148
|
+
"keywords": [
|
|
149
|
+
"byan_strict_complete",
|
|
150
|
+
"rejected below",
|
|
151
|
+
"earn"
|
|
152
|
+
],
|
|
153
|
+
"required": true
|
|
154
|
+
},
|
|
155
|
+
"priority": "critical"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "STRICT-11",
|
|
159
|
+
"title": "Honest Status",
|
|
160
|
+
"description": "Report failing tests with their output. Report skipped steps as skipped. Do not round a partial result up to \"done\".",
|
|
161
|
+
"validation": {
|
|
162
|
+
"type": "keyword",
|
|
163
|
+
"keywords": [
|
|
164
|
+
"honest status",
|
|
165
|
+
"failing tests",
|
|
166
|
+
"skipped"
|
|
167
|
+
],
|
|
168
|
+
"required": true
|
|
169
|
+
},
|
|
170
|
+
"priority": "high"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "STRICT-12",
|
|
174
|
+
"title": "Full Over Fast",
|
|
175
|
+
"description": "When the user states \"complete, no cost compromise\", speed is out of scope. Do not re-question the budget under the guise of an estimate.",
|
|
176
|
+
"validation": {
|
|
177
|
+
"type": "keyword",
|
|
178
|
+
"keywords": [
|
|
179
|
+
"full over fast",
|
|
180
|
+
"no cost compromise",
|
|
181
|
+
"out of scope"
|
|
182
|
+
],
|
|
183
|
+
"required": true
|
|
184
|
+
},
|
|
185
|
+
"priority": "high"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
@@ -5,25 +5,60 @@ class MantraValidator {
|
|
|
5
5
|
constructor(mantrasData = null) {
|
|
6
6
|
if (mantrasData) {
|
|
7
7
|
this.mantrasData = mantrasData;
|
|
8
|
+
this._explicitMantras = true;
|
|
8
9
|
} else {
|
|
9
10
|
const mantrasPath = path.join(__dirname, '../data/mantras.json');
|
|
10
11
|
this.mantrasData = JSON.parse(fs.readFileSync(mantrasPath, 'utf8'));
|
|
12
|
+
this._explicitMantras = false;
|
|
11
13
|
}
|
|
12
|
-
|
|
13
|
-
this.
|
|
14
|
+
|
|
15
|
+
this.personaMantras = this.mantrasData.mantras;
|
|
16
|
+
this.strictMantras = this._loadStrictMantras();
|
|
17
|
+
this.mantras = this.personaMantras;
|
|
14
18
|
this.results = null;
|
|
15
19
|
this.agentContent = null;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
// Strict-mode artifacts (the byan-strict skill, AGENTS.md block, copilot
|
|
23
|
+
// block) are validated against the strict mantra set, not the 64 persona
|
|
24
|
+
// mantras. The set is generated by byan-sync-rules from strict-mode.yaml.
|
|
25
|
+
_loadStrictMantras() {
|
|
26
|
+
try {
|
|
27
|
+
const p = path.join(__dirname, '../data/strict-mantras.json');
|
|
28
|
+
if (fs.existsSync(p)) {
|
|
29
|
+
return JSON.parse(fs.readFileSync(p, 'utf8')).mantras;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// fall through — no strict set available
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_isStrictArtifact(content) {
|
|
38
|
+
if (typeof content !== 'string') return false;
|
|
39
|
+
if (/name:\s*byan-strict/.test(content)) return true;
|
|
40
|
+
if (/#\s*BYAN Strict Mode/.test(content)) return true;
|
|
41
|
+
const strictIds = content.match(/STRICT-\d+/g) || [];
|
|
42
|
+
return strictIds.length >= 3;
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
validate(agentDefinition) {
|
|
19
46
|
if (agentDefinition === null || agentDefinition === undefined) {
|
|
20
47
|
throw new Error('Agent definition is required');
|
|
21
48
|
}
|
|
22
49
|
|
|
23
|
-
this.agentContent = typeof agentDefinition === 'string'
|
|
24
|
-
? agentDefinition
|
|
50
|
+
this.agentContent = typeof agentDefinition === 'string'
|
|
51
|
+
? agentDefinition
|
|
25
52
|
: JSON.stringify(agentDefinition);
|
|
26
53
|
|
|
54
|
+
// Pick the ruleset : strict artifacts get the strict mantras, unless the
|
|
55
|
+
// caller supplied an explicit mantra set in the constructor.
|
|
56
|
+
if (!this._explicitMantras && this.strictMantras && this._isStrictArtifact(this.agentContent)) {
|
|
57
|
+
this.mantras = this.strictMantras;
|
|
58
|
+
} else {
|
|
59
|
+
this.mantras = this.personaMantras;
|
|
60
|
+
}
|
|
61
|
+
|
|
27
62
|
this.results = {
|
|
28
63
|
totalMantras: this.mantras.length,
|
|
29
64
|
compliant: [],
|
|
@@ -13,6 +13,7 @@ const fs = require('fs-extra');
|
|
|
13
13
|
|
|
14
14
|
// Module under test (loaded once; all I/O goes to tmpdir per test)
|
|
15
15
|
const { runMigration } = require('../lib/migrate-mcp-config');
|
|
16
|
+
const { mcpConfig: { TOKEN_PLACEHOLDER } } = require('byan-platform-config');
|
|
16
17
|
|
|
17
18
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
18
19
|
// Helpers
|
|
@@ -69,16 +70,16 @@ test('returns no-byan-server if .mcp.json has other servers but no byan entry',
|
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
72
|
-
// Test 3 — byan entry already has
|
|
73
|
+
// Test 3 — byan entry already has placeholder + clean URL → already-ok
|
|
73
74
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
74
|
-
test('returns already-ok if byan entry has token
|
|
75
|
+
test('returns already-ok if byan entry has no BYAN_API_TOKEN (token belongs in .env / settings.local.json)', async () => {
|
|
75
76
|
const dir = await makeTmp();
|
|
76
77
|
await writeMcp(dir, {
|
|
77
78
|
mcpServers: {
|
|
78
79
|
byan: {
|
|
79
80
|
command: 'node',
|
|
80
81
|
args: ['_byan/mcp/byan-mcp-server/server.js'],
|
|
81
|
-
env: { BYAN_API_URL: 'https://api.byan.io'
|
|
82
|
+
env: { BYAN_API_URL: 'https://api.byan.io' },
|
|
82
83
|
},
|
|
83
84
|
},
|
|
84
85
|
});
|
|
@@ -91,7 +92,7 @@ test('returns already-ok if byan entry has token and no /api suffix', async () =
|
|
|
91
92
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
92
93
|
// Test 4 — needs token but .env + settings.local.json both empty → no-token-available
|
|
93
94
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
94
|
-
test('returns
|
|
95
|
+
test('returns already-ok if .mcp.json has no BYAN_API_TOKEN (token absence is now the desired state)', async () => {
|
|
95
96
|
const dir = await makeTmp();
|
|
96
97
|
await writeMcp(dir, {
|
|
97
98
|
mcpServers: {
|
|
@@ -99,23 +100,19 @@ test('returns no-token-available if .mcp.json needs token but sources are empty'
|
|
|
99
100
|
command: 'node',
|
|
100
101
|
args: ['_byan/mcp/byan-mcp-server/server.js'],
|
|
101
102
|
env: { BYAN_API_URL: 'https://api.byan.io' },
|
|
102
|
-
// no BYAN_API_TOKEN
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
105
|
});
|
|
106
|
-
// no .env, no settings.local.json
|
|
107
106
|
const result = await runMigration(dir);
|
|
108
107
|
expect(result.migrated).toBe(false);
|
|
109
|
-
expect(result.reason).toBe('
|
|
110
|
-
expect(typeof result.hint).toBe('string');
|
|
111
|
-
expect(result.hint.length).toBeGreaterThan(0);
|
|
108
|
+
expect(result.reason).toBe('already-ok');
|
|
112
109
|
await fs.remove(dir);
|
|
113
110
|
});
|
|
114
111
|
|
|
115
112
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
116
113
|
// Test 5 — migrates successfully: token from .env
|
|
117
114
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
118
|
-
test('
|
|
115
|
+
test('with token already absent in .mcp.json + .env populated: no-op (already-ok), .env preserved untouched', async () => {
|
|
119
116
|
const dir = await makeTmp();
|
|
120
117
|
await writeMcp(dir, {
|
|
121
118
|
mcpServers: {
|
|
@@ -129,19 +126,18 @@ test('migrates: token from .env → .mcp.json env.BYAN_API_TOKEN', async () => {
|
|
|
129
126
|
await writeDotenv(dir, 'BYAN_API_TOKEN=byan_from_dotenv\n');
|
|
130
127
|
|
|
131
128
|
const result = await runMigration(dir);
|
|
132
|
-
expect(result.migrated).toBe(
|
|
133
|
-
expect(result.reason).toBe('
|
|
134
|
-
expect(result.changes.length).toBeGreaterThanOrEqual(1);
|
|
129
|
+
expect(result.migrated).toBe(false);
|
|
130
|
+
expect(result.reason).toBe('already-ok');
|
|
135
131
|
|
|
136
|
-
const
|
|
137
|
-
expect(
|
|
132
|
+
const dotenvAfter = await fs.readFile(path.join(dir, '.env'), 'utf8');
|
|
133
|
+
expect(dotenvAfter).toBe('BYAN_API_TOKEN=byan_from_dotenv\n');
|
|
138
134
|
await fs.remove(dir);
|
|
139
135
|
});
|
|
140
136
|
|
|
141
137
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
142
138
|
// Test 6 — migrates successfully: token from .claude/settings.local.json fallback
|
|
143
139
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
144
|
-
test('
|
|
140
|
+
test('with token in settings.local.json + absent from .mcp.json: no-op (already-ok), settings preserved', async () => {
|
|
145
141
|
const dir = await makeTmp();
|
|
146
142
|
await writeMcp(dir, {
|
|
147
143
|
mcpServers: {
|
|
@@ -152,22 +148,20 @@ test('migrates: token from settings.local.json fallback', async () => {
|
|
|
152
148
|
},
|
|
153
149
|
},
|
|
154
150
|
});
|
|
155
|
-
// no .env, token in settings.local.json
|
|
156
151
|
await writeSettingsLocal(dir, { env: { BYAN_API_TOKEN: 'byan_from_settings' } });
|
|
157
152
|
|
|
158
153
|
const result = await runMigration(dir);
|
|
159
|
-
expect(result.migrated).toBe(
|
|
160
|
-
expect(result.reason).toBe('
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBe('byan_from_settings');
|
|
154
|
+
expect(result.migrated).toBe(false);
|
|
155
|
+
expect(result.reason).toBe('already-ok');
|
|
156
|
+
const settings = await fs.readJson(path.join(dir, '.claude', 'settings.local.json'));
|
|
157
|
+
expect(settings.env.BYAN_API_TOKEN).toBe('byan_from_settings');
|
|
164
158
|
await fs.remove(dir);
|
|
165
159
|
});
|
|
166
160
|
|
|
167
161
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
168
162
|
// Test 7 — strips /api suffix from BYAN_API_URL
|
|
169
163
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
170
|
-
test('strips /api suffix from
|
|
164
|
+
test('strips /api suffix and extracts clear token to .env (token removed from .mcp.json)', async () => {
|
|
171
165
|
const dir = await makeTmp();
|
|
172
166
|
await writeMcp(dir, {
|
|
173
167
|
mcpServers: {
|
|
@@ -184,7 +178,60 @@ test('strips /api suffix from BYAN_API_URL', async () => {
|
|
|
184
178
|
|
|
185
179
|
const written = await readMcp(dir);
|
|
186
180
|
expect(written.mcpServers.byan.env.BYAN_API_URL).toBe('https://api.byan.io');
|
|
187
|
-
expect(written.mcpServers.byan.env.BYAN_API_TOKEN).
|
|
181
|
+
expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
|
|
182
|
+
const dotenvContent = await fs.readFile(path.join(dir, '.env'), 'utf8');
|
|
183
|
+
expect(dotenvContent).toContain('BYAN_API_TOKEN=byan_tok');
|
|
184
|
+
const raw = await fs.readFile(path.join(dir, '.mcp.json'), 'utf8');
|
|
185
|
+
expect(raw).not.toContain('byan_tok');
|
|
186
|
+
await fs.remove(dir);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('extracts clear token from .mcp.json into .env and removes it from .mcp.json (security migration)', async () => {
|
|
190
|
+
const dir = await makeTmp();
|
|
191
|
+
await writeMcp(dir, {
|
|
192
|
+
mcpServers: {
|
|
193
|
+
byan: {
|
|
194
|
+
command: 'node',
|
|
195
|
+
args: ['_byan/mcp/byan-mcp-server/server.js'],
|
|
196
|
+
env: { BYAN_API_URL: 'https://api.byan.io', BYAN_API_TOKEN: 'byan_leaked_in_clear' },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const result = await runMigration(dir);
|
|
202
|
+
expect(result.migrated).toBe(true);
|
|
203
|
+
expect(result.reason).toBe('healed');
|
|
204
|
+
expect(result.changes.some((c) => /extracted/i.test(c))).toBe(true);
|
|
205
|
+
|
|
206
|
+
const written = await readMcp(dir);
|
|
207
|
+
expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
|
|
208
|
+
|
|
209
|
+
const raw = await fs.readFile(path.join(dir, '.mcp.json'), 'utf8');
|
|
210
|
+
expect(raw).not.toContain('byan_leaked_in_clear');
|
|
211
|
+
|
|
212
|
+
const dotenvContent = await fs.readFile(path.join(dir, '.env'), 'utf8');
|
|
213
|
+
expect(dotenvContent).toContain('BYAN_API_TOKEN=byan_leaked_in_clear');
|
|
214
|
+
await fs.remove(dir);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('removes ${BYAN_API_TOKEN} placeholder from .mcp.json (no value to extract)', async () => {
|
|
218
|
+
const dir = await makeTmp();
|
|
219
|
+
await writeMcp(dir, {
|
|
220
|
+
mcpServers: {
|
|
221
|
+
byan: {
|
|
222
|
+
command: 'node',
|
|
223
|
+
args: ['_byan/mcp/byan-mcp-server/server.js'],
|
|
224
|
+
env: { BYAN_API_URL: 'https://api.byan.io', BYAN_API_TOKEN: TOKEN_PLACEHOLDER },
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const result = await runMigration(dir);
|
|
230
|
+
expect(result.migrated).toBe(true);
|
|
231
|
+
expect(result.reason).toBe('healed');
|
|
232
|
+
|
|
233
|
+
const written = await readMcp(dir);
|
|
234
|
+
expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
|
|
188
235
|
await fs.remove(dir);
|
|
189
236
|
});
|
|
190
237
|
|
|
@@ -215,6 +262,9 @@ test('dry-run: does not write .mcp.json but returns changes', async () => {
|
|
|
215
262
|
const afterMcp = await readMcp(dir);
|
|
216
263
|
expect(afterMcp.mcpServers.byan.env.BYAN_API_URL).toBe('https://api.byan.io/api');
|
|
217
264
|
expect(afterMcp.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
|
|
265
|
+
// .env must NOT have been touched on dry-run either
|
|
266
|
+
const dotenvAfter = await fs.readFile(path.join(dir, '.env'), 'utf8');
|
|
267
|
+
expect(dotenvAfter).toBe('BYAN_API_TOKEN=byan_dryrun_tok\n');
|
|
218
268
|
await fs.remove(dir);
|
|
219
269
|
});
|
|
220
270
|
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* migrate-mcp-config — self-healing migration for pre-fix BYAN installs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Heals three drift cases on .mcp.json:
|
|
5
|
+
* 1. token missing → injects ${BYAN_API_TOKEN} placeholder
|
|
6
|
+
* 2. url has /api suffix → strips it
|
|
7
|
+
* 3. token in clear → extracts to .env and replaces with placeholder
|
|
8
|
+
* (security fix from BYAN >=2.16.0)
|
|
9
|
+
*
|
|
10
|
+
* Uses byan-platform-config primitives exclusively; no duplicated logic here.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
const chalk = require('chalk');
|
|
10
14
|
const ora = require('ora');
|
|
11
15
|
const {
|
|
12
|
-
mcpConfig: { readMcpConfig, ensureMcpConfig },
|
|
13
|
-
envConfig: { readEnvToken },
|
|
16
|
+
mcpConfig: { readMcpConfig, ensureMcpConfig, TOKEN_PLACEHOLDER },
|
|
17
|
+
envConfig: { readEnvToken, updateDotenv },
|
|
14
18
|
urlUtils: { stripApiSuffix },
|
|
15
19
|
} = require('byan-platform-config');
|
|
16
20
|
|
|
@@ -65,32 +69,27 @@ async function runMigration(projectRoot, { dryRun = false, verbose = false } = {
|
|
|
65
69
|
stopSpinner(chalk.gray('.mcp.json lu'), true);
|
|
66
70
|
|
|
67
71
|
// 3. Diagnose
|
|
68
|
-
const
|
|
69
|
-
const
|
|
72
|
+
const currentToken = (byan.env && byan.env.BYAN_API_TOKEN) || '';
|
|
73
|
+
const tokenIsPlaceholder = currentToken === TOKEN_PLACEHOLDER;
|
|
74
|
+
const tokenInClear = !!currentToken && !tokenIsPlaceholder;
|
|
75
|
+
const tokenIsLegacy = !!currentToken; // any non-empty value is legacy: token must not live in .mcp.json
|
|
76
|
+
const urlHasApiSuffix = /\/api\/?$/.test(byan.env && byan.env.BYAN_API_URL || '');
|
|
70
77
|
|
|
71
78
|
// 4. Nothing to do?
|
|
72
|
-
if (!
|
|
73
|
-
log(chalk.green(' .mcp.json est deja a jour (token
|
|
79
|
+
if (!tokenIsLegacy && !urlHasApiSuffix) {
|
|
80
|
+
log(chalk.green(' .mcp.json est deja a jour (token absent du fichier, URL correcte)'));
|
|
74
81
|
return { migrated: false, reason: 'already-ok' };
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
const changes = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
migrated: false,
|
|
88
|
-
reason: 'no-token-available',
|
|
89
|
-
hint: 'Re-run npx create-byan-agent to prompt for a token',
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
stopSpinner(chalk.green('Token trouve'), true);
|
|
93
|
-
changes.push('BYAN_API_TOKEN injected into .mcp.json env');
|
|
85
|
+
let extractedClearToken = null;
|
|
86
|
+
|
|
87
|
+
// 5. Detect a clear-text token to relocate to .env
|
|
88
|
+
if (tokenInClear) {
|
|
89
|
+
extractedClearToken = currentToken;
|
|
90
|
+
changes.push('BYAN_API_TOKEN extracted from .mcp.json (clear) into .env, removed from .mcp.json');
|
|
91
|
+
} else if (tokenIsPlaceholder) {
|
|
92
|
+
changes.push('BYAN_API_TOKEN placeholder removed from .mcp.json (token now resolved via settings.local.json env)');
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
// 6. Resolve URL
|
|
@@ -108,9 +107,16 @@ async function runMigration(projectRoot, { dryRun = false, verbose = false } = {
|
|
|
108
107
|
return { migrated: false, reason: 'dry-run', changes };
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
// 8. Apply
|
|
110
|
+
// 8. Apply
|
|
112
111
|
startSpinner('Application de la migration...');
|
|
113
|
-
|
|
112
|
+
if (extractedClearToken) {
|
|
113
|
+
const existingDotenvToken = await readEnvToken(projectRoot);
|
|
114
|
+
if (!existingDotenvToken || existingDotenvToken !== extractedClearToken) {
|
|
115
|
+
await updateDotenv(projectRoot, { BYAN_API_TOKEN: extractedClearToken });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ensureMcpConfig always strips BYAN_API_TOKEN from .mcp.json (security)
|
|
119
|
+
await ensureMcpConfig(projectRoot, { apiUrl: cleanUrl || existingUrl });
|
|
114
120
|
stopSpinner(chalk.green('.mcp.json migre avec succes'), true);
|
|
115
121
|
|
|
116
122
|
// 9. Return result
|