@envseal/cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +7 -0
- package/dist/commands/audit.d.ts +16 -0
- package/dist/commands/audit.js +117 -0
- package/dist/commands/doctor.js +77 -5
- package/dist/commands/status.js +20 -1
- package/dist/exit-codes.d.ts +1 -0
- package/dist/exit-codes.js +6 -0
- package/dist/host-wiring/copilot.js +10 -1
- package/dist/host-wiring/mcp.d.ts +12 -0
- package/dist/host-wiring/mcp.js +13 -1
- package/dist/host-wiring/zed.js +5 -2
- package/package.json +8 -8
package/dist/bin.js
CHANGED
|
@@ -11,6 +11,7 @@ import { verify } from './commands/verify.js';
|
|
|
11
11
|
import { run } from './commands/run.js';
|
|
12
12
|
import { doctor } from './commands/doctor.js';
|
|
13
13
|
import { revoke } from './commands/revoke.js';
|
|
14
|
+
import { audit } from './commands/audit.js';
|
|
14
15
|
import { mcp } from './commands/mcp.js';
|
|
15
16
|
import { init } from './commands/init.js';
|
|
16
17
|
const VERSION = '0.1.5';
|
|
@@ -115,6 +116,10 @@ async function main() {
|
|
|
115
116
|
await revoke(root, key, json, parsed.flags.yes === true);
|
|
116
117
|
break;
|
|
117
118
|
}
|
|
119
|
+
case 'audit': {
|
|
120
|
+
await audit(root, json, parsed.flags.verify === true);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
118
123
|
case 'mcp': {
|
|
119
124
|
await mcp(root);
|
|
120
125
|
break;
|
|
@@ -146,6 +151,8 @@ Commands:
|
|
|
146
151
|
run -- <cmd...> Execute command with injected secrets
|
|
147
152
|
doctor Report project configuration status
|
|
148
153
|
revoke <KEY> Revoke a key from the sink
|
|
154
|
+
audit [--verify] Print audit events (--json for machine form);
|
|
155
|
+
--verify checks the log's tamper-evidence chain
|
|
149
156
|
mcp Start the MCP server
|
|
150
157
|
|
|
151
158
|
Global Options:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `envseal audit` — inspect the project's audit log.
|
|
3
|
+
*
|
|
4
|
+
* Default: print recorded events (human-readable) or the raw event array
|
|
5
|
+
* (--json). With --verify: check the hash chain instead; exit 7
|
|
6
|
+
* (AUDIT_CHAIN_FAILED) when the chain is broken. A missing log verifies as
|
|
7
|
+
* intact with zero records — there is nothing to attest, and "no log yet"
|
|
8
|
+
* must not look like tampering.
|
|
9
|
+
*
|
|
10
|
+
* When the project's out-of-band mirror (~/.envseal/mirrors/) exists, verify
|
|
11
|
+
* also compares the log against it: the mirror is a second copy the project's
|
|
12
|
+
* agent cannot silently shrink, so records the mirror proves existed but the
|
|
13
|
+
* log lost are tail truncation — exit 7. See docs/residual-risks.md §10.
|
|
14
|
+
*/
|
|
15
|
+
export declare function audit(root: string, json: boolean, verifyMode: boolean): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { compareWithMirror, projectPaths, readAudit, readMirrorLines, verifyAuditChain } from '@envseal/core';
|
|
4
|
+
import { EXIT } from '../exit-codes.js';
|
|
5
|
+
import { finish } from '../exit.js';
|
|
6
|
+
/**
|
|
7
|
+
* `envseal audit` — inspect the project's audit log.
|
|
8
|
+
*
|
|
9
|
+
* Default: print recorded events (human-readable) or the raw event array
|
|
10
|
+
* (--json). With --verify: check the hash chain instead; exit 7
|
|
11
|
+
* (AUDIT_CHAIN_FAILED) when the chain is broken. A missing log verifies as
|
|
12
|
+
* intact with zero records — there is nothing to attest, and "no log yet"
|
|
13
|
+
* must not look like tampering.
|
|
14
|
+
*
|
|
15
|
+
* When the project's out-of-band mirror (~/.envseal/mirrors/) exists, verify
|
|
16
|
+
* also compares the log against it: the mirror is a second copy the project's
|
|
17
|
+
* agent cannot silently shrink, so records the mirror proves existed but the
|
|
18
|
+
* log lost are tail truncation — exit 7. See docs/residual-risks.md §10.
|
|
19
|
+
*/
|
|
20
|
+
export async function audit(root, json, verifyMode) {
|
|
21
|
+
if (!verifyMode) {
|
|
22
|
+
// No manifest gate here on purpose: the log is written by provisioning and
|
|
23
|
+
// use flows, and it stays readable even in a half-torn-down project whose
|
|
24
|
+
// env.schema.jsonc is already gone. An audit surface that refuses to
|
|
25
|
+
// audit would be its own finding.
|
|
26
|
+
const events = readAudit(projectPaths(root));
|
|
27
|
+
if (json) {
|
|
28
|
+
console.log(JSON.stringify(events, null, 0));
|
|
29
|
+
finish(EXIT.OK);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (events.length === 0) {
|
|
33
|
+
console.log('No audit events recorded.');
|
|
34
|
+
finish(EXIT.OK);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const e of events) {
|
|
38
|
+
console.log(formatEvent(e));
|
|
39
|
+
}
|
|
40
|
+
finish(EXIT.OK);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// --verify mode: chain check over the raw bytes. A missing file is an empty
|
|
44
|
+
// chain, not an error (see doc comment).
|
|
45
|
+
let raw = '';
|
|
46
|
+
try {
|
|
47
|
+
raw = readFileSync(join(root, '.envseal', 'audit.jsonl'), 'utf8');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
raw = '';
|
|
51
|
+
}
|
|
52
|
+
const result = verifyAuditChain(raw);
|
|
53
|
+
const mirror = compareWithMirror(raw, readMirrorLines(root));
|
|
54
|
+
const ok = result.ok && !mirror.tailTruncated;
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify(!ok
|
|
57
|
+
? {
|
|
58
|
+
ok: false,
|
|
59
|
+
brokenAt: result.ok ? null : (result.brokenAt ?? null),
|
|
60
|
+
count: result.count,
|
|
61
|
+
mirror: { present: mirror.mirrorPresent, records: mirror.mirrorRecords },
|
|
62
|
+
}
|
|
63
|
+
: { ok: true, count: result.count, mirror: { present: mirror.mirrorPresent, records: mirror.mirrorRecords } }, null, 0));
|
|
64
|
+
finish(ok ? EXIT.OK : EXIT.AUDIT_CHAIN_FAILED);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!ok) {
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
console.error(`AUDIT CHAIN FAILED: first break at record ${result.brokenAt} of ${result.count}. ` +
|
|
70
|
+
'Records were edited, deleted, reordered, or spliced after the fact. ' +
|
|
71
|
+
'Treat every record after the break as untrusted and investigate the host.');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.error(`AUDIT TAIL LOST: the project log holds ${mirror.projectRecords} record(s) but its out-of-band mirror ` +
|
|
75
|
+
`attests ${mirror.mirrorRecords}. Records after the surviving tail were deleted after being mirrored. ` +
|
|
76
|
+
'Treat the log as incomplete and investigate the host (docs/residual-risks.md §10).');
|
|
77
|
+
}
|
|
78
|
+
finish(EXIT.AUDIT_CHAIN_FAILED);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (mirror.mirrorPresent && mirror.mirrorRecords > mirror.projectRecords) {
|
|
82
|
+
console.log(`Audit chain intact (${result.count} record${result.count === 1 ? '' : 's'}); mirror holds ` +
|
|
83
|
+
`${mirror.mirrorRecords} — pre-reset history, not tampering.`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log(`Audit chain intact (${result.count} record${result.count === 1 ? '' : 's'}).`);
|
|
87
|
+
}
|
|
88
|
+
finish(EXIT.OK);
|
|
89
|
+
}
|
|
90
|
+
function formatEvent(e) {
|
|
91
|
+
const at = e.at;
|
|
92
|
+
switch (e.type) {
|
|
93
|
+
case 'declare':
|
|
94
|
+
return `${at} declare keys=${JSON.stringify(e.keys)}`;
|
|
95
|
+
case 'request':
|
|
96
|
+
return `${at} request ticket=${e.ticket} keys=${JSON.stringify(e.keys)} surface=${e.surface}`;
|
|
97
|
+
case 'stored':
|
|
98
|
+
return `${at} stored key=${e.key} sink=${e.sink}`;
|
|
99
|
+
case 'skipped':
|
|
100
|
+
case 'cancelled':
|
|
101
|
+
case 'timeout':
|
|
102
|
+
return `${at} ${e.type} ticket=${e.ticket} key=${e.key}`;
|
|
103
|
+
case 'verify':
|
|
104
|
+
return `${at} verify key=${e.key} result=${e.result}`;
|
|
105
|
+
case 'revoke':
|
|
106
|
+
return `${at} revoke key=${e.key} sink=${e.sink}`;
|
|
107
|
+
case 'blocked':
|
|
108
|
+
return `${at} blocked reason=${e.reason}`;
|
|
109
|
+
case 'use':
|
|
110
|
+
return `${at} use keys=${JSON.stringify(e.keys)} networkEgress=${String(e.networkEgress)} cmd=${e.command}`;
|
|
111
|
+
case 'use_result':
|
|
112
|
+
return `${at} use_result exit=${String(e.exitCode)} signal=${String(e.signal)} ${e.durationMs}ms`;
|
|
113
|
+
default:
|
|
114
|
+
return `${at} ${e.type}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=audit.js.map
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { SepError } from '@envseal/protocol';
|
|
4
|
-
import { inspectDotenvGitSafety, projectPaths } from '@envseal/core';
|
|
4
|
+
import { inspectDotenvGitSafety, loadManifest, projectPaths, readHookDecisions, readHookHeartbeat } from '@envseal/core';
|
|
5
5
|
import { emit, fail } from '../output.js';
|
|
6
6
|
import { EXIT } from '../exit-codes.js';
|
|
7
7
|
import { detectHost } from '../host.js';
|
|
@@ -22,6 +22,7 @@ export async function doctor(root, json) {
|
|
|
22
22
|
}));
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
+
const manifest = loadManifest(projectPaths(root));
|
|
25
26
|
const broker = await createBroker(root);
|
|
26
27
|
const status = await broker.describe();
|
|
27
28
|
const gitignorePath = join(root, '.gitignore');
|
|
@@ -29,14 +30,26 @@ export async function doctor(root, json) {
|
|
|
29
30
|
const gitSafety = inspectDotenvGitSafety(projectPaths(root));
|
|
30
31
|
const gitignoreCovers = gitSafety.ignored;
|
|
31
32
|
const hookFailClosed = process.env.ENVSEAL_HOOK_FAIL_CLOSED === '1';
|
|
32
|
-
// Check .env permissions
|
|
33
|
-
|
|
33
|
+
// Check .env permissions.
|
|
34
|
+
//
|
|
35
|
+
// POSIX mode bits are only enforced on POSIX. Windows statSync still
|
|
36
|
+
// reports a mode (0o666 writable, 0o444 read-only), so the group/other
|
|
37
|
+
// test would produce 0o066 ≠ 0 and report permissionsOk:false on EVERY
|
|
38
|
+
// Windows machine regardless of the real ACLs — a permanent false alarm.
|
|
39
|
+
// Report null ("not measurable here") instead; access on Windows is an
|
|
40
|
+
// ACL question this check cannot answer.
|
|
41
|
+
let envFileOk = null;
|
|
34
42
|
if (existsSync(envPath)) {
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
if (process.platform !== 'win32') {
|
|
44
|
+
const stats = statSync(envPath);
|
|
45
|
+
envFileOk = (stats.mode & 0o077) === 0;
|
|
46
|
+
}
|
|
37
47
|
}
|
|
38
48
|
const host = detectHost(root);
|
|
49
|
+
const egress = manifest?.policy?.egress;
|
|
39
50
|
const inspection = inspectPrimaryHostWiring(root, host.id, { probe: true });
|
|
51
|
+
const hookLastRan = readHookHeartbeat(root);
|
|
52
|
+
const hookDecisions = readHookDecisions(root);
|
|
40
53
|
const output = {
|
|
41
54
|
projectRoot: root,
|
|
42
55
|
manifestPath,
|
|
@@ -57,9 +70,15 @@ export async function doctor(root, json) {
|
|
|
57
70
|
isTracked: gitSafety.tracked,
|
|
58
71
|
permissionsOk: envFileOk,
|
|
59
72
|
},
|
|
73
|
+
egressPolicy: egress ?? { mode: 'warn', allow: [] },
|
|
60
74
|
hookFailClosed,
|
|
75
|
+
hookLastRan,
|
|
76
|
+
hookDecisions,
|
|
61
77
|
missingRequiredCount: status.missingRequired.length,
|
|
62
78
|
missingRequired: status.missingRequired,
|
|
79
|
+
rotationOverdue: status.entries
|
|
80
|
+
.filter((e) => isOverdue(e.rotationDue))
|
|
81
|
+
.map((e) => ({ key: e.key, due: e.rotationDue })),
|
|
63
82
|
...(inspection.mcp === undefined
|
|
64
83
|
? {}
|
|
65
84
|
: {
|
|
@@ -68,6 +87,9 @@ export async function doctor(root, json) {
|
|
|
68
87
|
status: inspection.mcp.status,
|
|
69
88
|
message: inspection.mcp.message,
|
|
70
89
|
commandOk: inspection.mcp.commandOk,
|
|
90
|
+
...(inspection.mcp.otherServers === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { otherServers: inspection.mcp.otherServers }),
|
|
71
93
|
},
|
|
72
94
|
}),
|
|
73
95
|
};
|
|
@@ -82,13 +104,29 @@ export async function doctor(root, json) {
|
|
|
82
104
|
}
|
|
83
105
|
console.log(` ${inspection.message}`);
|
|
84
106
|
console.log(`Gitignore covers .env: ${gitignoreCovers ? 'yes' : 'no'}`);
|
|
107
|
+
console.log(`Egress policy: ${egress?.mode === 'allowlist' ? `allowlist (${egress.allow.length} allowed host${egress.allow.length === 1 ? '' : 's'})` : 'warn (default)'}`);
|
|
85
108
|
console.log(`Hook on internal error: ${hookFailClosed ? 'fail-closed' : 'fail-open (default)'}`);
|
|
109
|
+
console.log(`Hook heartbeat: ${describeHeartbeatAge(hookLastRan)}`);
|
|
110
|
+
if (hookDecisions !== null) {
|
|
111
|
+
console.log(`Hook decisions (approx): ${hookDecisions.allow} allow, ${hookDecisions.deny} deny`);
|
|
112
|
+
}
|
|
113
|
+
const siblings = inspection.mcp?.otherServers ?? [];
|
|
114
|
+
if (siblings.length > 0) {
|
|
115
|
+
console.log(`Other MCP servers (outside envseal read protection): ${siblings.join(', ')}`);
|
|
116
|
+
}
|
|
86
117
|
console.log(`Missing required keys: ${status.missingRequired.length}`);
|
|
87
118
|
if (status.missingRequired.length > 0) {
|
|
88
119
|
for (const key of status.missingRequired) {
|
|
89
120
|
console.log(` - ${key}`);
|
|
90
121
|
}
|
|
91
122
|
}
|
|
123
|
+
const overdue = status.entries.filter((e) => e.rotationDue !== null && isOverdue(e.rotationDue));
|
|
124
|
+
if (overdue.length > 0) {
|
|
125
|
+
console.log('Rotation overdue (advisory — rotate the credential, then rewrite the value):');
|
|
126
|
+
for (const e of overdue) {
|
|
127
|
+
console.log(` - ${e.key}: due ${e.rotationDue.slice(0, 10)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
92
130
|
}
|
|
93
131
|
else {
|
|
94
132
|
emit(json, '', output);
|
|
@@ -102,4 +140,38 @@ export async function doctor(root, json) {
|
|
|
102
140
|
fail(json, error);
|
|
103
141
|
}
|
|
104
142
|
}
|
|
143
|
+
/** Advisory by design: overdue rotation never fails doctor the way a
|
|
144
|
+
* missing required key does, because an aged-but-working credential is a
|
|
145
|
+
* hygiene problem, not an outage. */
|
|
146
|
+
function isOverdue(rotationDue) {
|
|
147
|
+
if (rotationDue === null)
|
|
148
|
+
return false;
|
|
149
|
+
const due = Date.parse(rotationDue);
|
|
150
|
+
return !Number.isNaN(due) && due <= Date.now();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Human phrasing for the hook heartbeat. Advisory only — wiring can be
|
|
154
|
+
* present while the hook has never run (no plugin version, no tool call yet),
|
|
155
|
+
* and a recent timestamp proves liveness, not correctness.
|
|
156
|
+
*/
|
|
157
|
+
function describeHeartbeatAge(hookLastRan) {
|
|
158
|
+
if (hookLastRan === null) {
|
|
159
|
+
return 'none recorded (hook has not run for this project, or pre-heartbeat plugin)';
|
|
160
|
+
}
|
|
161
|
+
const then = Date.parse(hookLastRan);
|
|
162
|
+
if (Number.isNaN(then)) {
|
|
163
|
+
return 'unreadable timestamp';
|
|
164
|
+
}
|
|
165
|
+
const seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
|
|
166
|
+
if (seconds < 90)
|
|
167
|
+
return `${seconds}s ago`;
|
|
168
|
+
const minutes = Math.round(seconds / 60);
|
|
169
|
+
if (minutes < 90)
|
|
170
|
+
return `${minutes}m ago`;
|
|
171
|
+
const hours = Math.round(minutes / 60);
|
|
172
|
+
if (hours < 36)
|
|
173
|
+
return `${hours}h ago`;
|
|
174
|
+
const days = Math.round(hours / 24);
|
|
175
|
+
return `${days}d ago`;
|
|
176
|
+
}
|
|
105
177
|
//# sourceMappingURL=doctor.js.map
|
package/dist/commands/status.js
CHANGED
|
@@ -18,7 +18,8 @@ export async function status(root, keys, json) {
|
|
|
18
18
|
else {
|
|
19
19
|
for (const entry of entriesToShow) {
|
|
20
20
|
const status_str = entry.present ? '✓' : '✗';
|
|
21
|
-
|
|
21
|
+
const due = describeRotation(entry.rotationDue);
|
|
22
|
+
console.log(`${status_str} ${entry.key}${due}`);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -33,6 +34,7 @@ export async function status(root, keys, json) {
|
|
|
33
34
|
fingerprint: e.fingerprint,
|
|
34
35
|
lastVerified: e.lastVerified,
|
|
35
36
|
verifyResult: e.verifyResult,
|
|
37
|
+
rotationDue: e.rotationDue,
|
|
36
38
|
})),
|
|
37
39
|
});
|
|
38
40
|
}
|
|
@@ -46,4 +48,21 @@ export async function status(root, keys, json) {
|
|
|
46
48
|
fail(json, error);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Overdue rotation is the only state worth a human's glance in the terse
|
|
53
|
+
* listing; a future due date is noise. Absent policy or unknown age (hand
|
|
54
|
+
* written .env before first status) reports nothing.
|
|
55
|
+
*/
|
|
56
|
+
function describeRotation(rotationDue) {
|
|
57
|
+
if (rotationDue === null)
|
|
58
|
+
return '';
|
|
59
|
+
const due = Date.parse(rotationDue);
|
|
60
|
+
if (Number.isNaN(due))
|
|
61
|
+
return '';
|
|
62
|
+
if (due > Date.now())
|
|
63
|
+
return '';
|
|
64
|
+
const days = Math.floor((Date.now() - due) / (24 * 60 * 60 * 1000));
|
|
65
|
+
const when = days === 0 ? 'today' : `${days}d ago`;
|
|
66
|
+
return ` (rotation overdue, due ${rotationDue.slice(0, 10)}, ${when})`;
|
|
67
|
+
}
|
|
49
68
|
//# sourceMappingURL=status.js.map
|
package/dist/exit-codes.d.ts
CHANGED
package/dist/exit-codes.js
CHANGED
|
@@ -7,6 +7,7 @@ export const EXIT = {
|
|
|
7
7
|
NO_SURFACE: 4,
|
|
8
8
|
SINK_FAILURE: 5,
|
|
9
9
|
VERIFY_FAILED: 6,
|
|
10
|
+
AUDIT_CHAIN_FAILED: 7,
|
|
10
11
|
};
|
|
11
12
|
export function exitCodeForError(e) {
|
|
12
13
|
if (!isSepError(e)) {
|
|
@@ -39,6 +40,11 @@ export function exitCodeForError(e) {
|
|
|
39
40
|
case 'SEP_CONFIRMATION_DENIED':
|
|
40
41
|
case 'SEP_KEYS_MISSING':
|
|
41
42
|
return EXIT.UNSATISFIED;
|
|
43
|
+
case 'SEP_EGRESS_DENIED':
|
|
44
|
+
// The project's standing policy refused the command before anything
|
|
45
|
+
// ran. Not retriable by re-running: the policy file must change first,
|
|
46
|
+
// so this maps to USAGE (configuration), not a transient failure.
|
|
47
|
+
return EXIT.USAGE;
|
|
42
48
|
default: {
|
|
43
49
|
const _exhaustive = code;
|
|
44
50
|
return _exhaustive;
|
|
@@ -79,11 +79,20 @@ export function inspectCopilotSettings(root, options = {}) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
const entry = list.find((item) => entryName(item) === ENVSEAL_MCP_SERVER_NAME);
|
|
82
|
+
// Names only, never entry values: a sibling argv or env block can hold a
|
|
83
|
+
// real credential, and doctor output must not become a new exfil channel.
|
|
84
|
+
// List configs can repeat a name; report each server once.
|
|
85
|
+
const otherServers = [
|
|
86
|
+
...new Set(list
|
|
87
|
+
.map((item) => entryName(item))
|
|
88
|
+
.filter((name) => name !== undefined && name !== ENVSEAL_MCP_SERVER_NAME)),
|
|
89
|
+
].sort();
|
|
82
90
|
if (entry === undefined || isEmptyEnvsealStub(entry) || !looksLikeEnvsealServer(entry)) {
|
|
83
91
|
return {
|
|
84
92
|
wired: false,
|
|
85
93
|
status: entry === undefined ? 'missing' : 'stub',
|
|
86
94
|
commandOk: null,
|
|
95
|
+
otherServers,
|
|
87
96
|
message: `.vscode/settings.json github.copilot.mcp has no working envseal-mcp. ${hint}`,
|
|
88
97
|
};
|
|
89
98
|
}
|
|
@@ -94,6 +103,6 @@ export function inspectCopilotSettings(root, options = {}) {
|
|
|
94
103
|
message =
|
|
95
104
|
'Copilot MCP is configured, but the launch command did not report a version. Run `envseal init`. [VERIFY]';
|
|
96
105
|
}
|
|
97
|
-
return { wired: true, status: 'wired', commandOk, message };
|
|
106
|
+
return { wired: true, status: 'wired', commandOk, otherServers, message };
|
|
98
107
|
}
|
|
99
108
|
//# sourceMappingURL=copilot.js.map
|
|
@@ -13,7 +13,19 @@ export type McpInspection = {
|
|
|
13
13
|
message: string;
|
|
14
14
|
/** null when the launch command was not probed (npx is not side-effect free). */
|
|
15
15
|
commandOk: boolean | null;
|
|
16
|
+
/**
|
|
17
|
+
* Names of co-registered MCP servers besides envseal-mcp. Names only, never
|
|
18
|
+
* entry values: a sibling argv or env block can hold a real credential, and
|
|
19
|
+
* doctor output must not become a new exfil channel. Undefined when the host
|
|
20
|
+
* config shape carries no enumerable server list.
|
|
21
|
+
*/
|
|
22
|
+
otherServers?: string[];
|
|
16
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Sorted names of every server in a `mcpServers`-style map except envseal-mcp.
|
|
26
|
+
* Keys only: callers must never forward the entry values.
|
|
27
|
+
*/
|
|
28
|
+
export declare function siblingServerNames(servers: Record<string, unknown>): string[];
|
|
17
29
|
/**
|
|
18
30
|
* Launch argv a host can spawn without a global `envseal-mcp` on PATH.
|
|
19
31
|
* Project MCP uses the workspace as cwd — never bake `--project` in.
|
package/dist/host-wiring/mcp.js
CHANGED
|
@@ -4,6 +4,15 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
export const ENVSEAL_MCP_PACKAGE = '@envseal/mcp-server';
|
|
5
5
|
export const ENVSEAL_MCP_SERVER_NAME = 'envseal-mcp';
|
|
6
6
|
export const NPX_ARGS = ['-y', ENVSEAL_MCP_PACKAGE];
|
|
7
|
+
/**
|
|
8
|
+
* Sorted names of every server in a `mcpServers`-style map except envseal-mcp.
|
|
9
|
+
* Keys only: callers must never forward the entry values.
|
|
10
|
+
*/
|
|
11
|
+
export function siblingServerNames(servers) {
|
|
12
|
+
return Object.keys(servers)
|
|
13
|
+
.filter((name) => name !== ENVSEAL_MCP_SERVER_NAME)
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* Launch argv a host can spawn without a global `envseal-mcp` on PATH.
|
|
9
18
|
* Project MCP uses the workspace as cwd — never bake `--project` in.
|
|
@@ -203,11 +212,13 @@ export function inspectMcpServersFile(path, label, options = {}) {
|
|
|
203
212
|
}
|
|
204
213
|
const entry = servers[ENVSEAL_MCP_SERVER_NAME];
|
|
205
214
|
const kind = classifyEntry(entry);
|
|
215
|
+
const otherServers = siblingServerNames(servers);
|
|
206
216
|
if (kind === 'missing') {
|
|
207
217
|
return {
|
|
208
218
|
wired: false,
|
|
209
219
|
status: 'missing',
|
|
210
220
|
commandOk: null,
|
|
221
|
+
otherServers,
|
|
211
222
|
message: `${label} has no envseal-mcp. ${hint}`,
|
|
212
223
|
};
|
|
213
224
|
}
|
|
@@ -216,6 +227,7 @@ export function inspectMcpServersFile(path, label, options = {}) {
|
|
|
216
227
|
wired: false,
|
|
217
228
|
status: 'stub',
|
|
218
229
|
commandOk: null,
|
|
230
|
+
otherServers,
|
|
219
231
|
message: `${label} envseal-mcp is the empty envseal-mcp stub (not on PATH for the host). ${hint}`,
|
|
220
232
|
};
|
|
221
233
|
}
|
|
@@ -225,7 +237,7 @@ export function inspectMcpServersFile(path, label, options = {}) {
|
|
|
225
237
|
if (commandOk === false) {
|
|
226
238
|
message = `MCP is configured in ${label}, but the launch command did not report a version. Run \`envseal init\` if the host cannot connect.`;
|
|
227
239
|
}
|
|
228
|
-
return { wired: true, status: 'wired', commandOk, message };
|
|
240
|
+
return { wired: true, status: 'wired', commandOk, otherServers, message };
|
|
229
241
|
}
|
|
230
242
|
/** Map an inspection to the doctor `agentWiring.mcp` field. */
|
|
231
243
|
export function mcpWiringState(inspection) {
|
package/dist/host-wiring/zed.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { ENVSEAL_MCP_SERVER_NAME, isRecord, mcpLaunch, nextEnvsealEntry, parseJsonObject, writeJson, classifyEntry, probeVersion, } from './mcp.js';
|
|
3
|
+
import { ENVSEAL_MCP_SERVER_NAME, isRecord, mcpLaunch, nextEnvsealEntry, parseJsonObject, writeJson, classifyEntry, probeVersion, siblingServerNames, } from './mcp.js';
|
|
4
4
|
function zedHint(platform) {
|
|
5
5
|
const launch = mcpLaunch(platform);
|
|
6
6
|
return `Run \`envseal init\` to merge { "mcp": { "${ENVSEAL_MCP_SERVER_NAME}": ${JSON.stringify(launch)} } } into .zed/settings.json. [VERIFY]`;
|
|
@@ -69,11 +69,13 @@ export function inspectZedSettings(root, options = {}) {
|
|
|
69
69
|
}
|
|
70
70
|
const entry = mcp[ENVSEAL_MCP_SERVER_NAME];
|
|
71
71
|
const kind = classifyEntry(entry);
|
|
72
|
+
const otherServers = siblingServerNames(mcp);
|
|
72
73
|
if (kind === 'missing') {
|
|
73
74
|
return {
|
|
74
75
|
wired: false,
|
|
75
76
|
status: 'missing',
|
|
76
77
|
commandOk: null,
|
|
78
|
+
otherServers,
|
|
77
79
|
message: `.zed/settings.json has no envseal-mcp. ${hint}`,
|
|
78
80
|
};
|
|
79
81
|
}
|
|
@@ -82,6 +84,7 @@ export function inspectZedSettings(root, options = {}) {
|
|
|
82
84
|
wired: false,
|
|
83
85
|
status: 'stub',
|
|
84
86
|
commandOk: null,
|
|
87
|
+
otherServers,
|
|
85
88
|
message: `.zed/settings.json envseal-mcp is the empty stub. ${hint}`,
|
|
86
89
|
};
|
|
87
90
|
}
|
|
@@ -92,6 +95,6 @@ export function inspectZedSettings(root, options = {}) {
|
|
|
92
95
|
message =
|
|
93
96
|
'Zed MCP is configured, but the launch command did not report a version. Run `envseal init`. [VERIFY]';
|
|
94
97
|
}
|
|
95
|
-
return { wired: true, status: 'wired', commandOk, message };
|
|
98
|
+
return { wired: true, status: 'wired', commandOk, otherServers, message };
|
|
96
99
|
}
|
|
97
100
|
//# sourceMappingURL=zed.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envseal/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"provenance": true
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@envseal/
|
|
27
|
-
"@envseal/
|
|
28
|
-
"@envseal/
|
|
29
|
-
"@envseal/detector": "0.1.
|
|
30
|
-
"@envseal/
|
|
31
|
-
"@envseal/http-server": "0.1.
|
|
32
|
-
"@envseal/
|
|
26
|
+
"@envseal/core": "0.1.7",
|
|
27
|
+
"@envseal/prompters": "0.1.7",
|
|
28
|
+
"@envseal/protocol": "0.1.7",
|
|
29
|
+
"@envseal/detector": "0.1.7",
|
|
30
|
+
"@envseal/registry": "0.1.7",
|
|
31
|
+
"@envseal/http-server": "0.1.7",
|
|
32
|
+
"@envseal/mcp-server": "0.1.7"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|