@envseal/cli 0.1.4 → 0.1.6
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 +9 -2
- package/dist/cli-utils.js +4 -4
- package/dist/commands/audit.d.ts +16 -0
- package/dist/commands/audit.js +117 -0
- package/dist/commands/doctor.js +84 -7
- package/dist/commands/init.js +70 -42
- 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/agents-md-content.d.ts +9 -0
- package/dist/host-wiring/agents-md-content.js +73 -0
- package/dist/host-wiring/agents-md.d.ts +20 -0
- package/dist/host-wiring/agents-md.js +50 -0
- package/dist/host-wiring/aider-conf.d.ts +8 -0
- package/dist/host-wiring/aider-conf.js +37 -0
- package/dist/host-wiring/aider.d.ts +14 -0
- package/dist/host-wiring/aider.js +90 -0
- package/dist/host-wiring/apply.d.ts +22 -0
- package/dist/host-wiring/apply.js +165 -0
- package/dist/host-wiring/codex.d.ts +8 -0
- package/dist/host-wiring/codex.js +57 -0
- package/dist/host-wiring/continue.d.ts +9 -0
- package/dist/host-wiring/continue.js +54 -0
- package/dist/host-wiring/copilot.d.ts +10 -0
- package/dist/host-wiring/copilot.js +99 -0
- package/dist/host-wiring/cursor-rules.d.ts +9 -0
- package/dist/host-wiring/cursor-rules.js +31 -0
- package/dist/host-wiring/cursor.d.ts +31 -0
- package/dist/host-wiring/cursor.js +39 -0
- package/dist/host-wiring/goose.d.ts +16 -0
- package/dist/host-wiring/goose.js +57 -0
- package/dist/host-wiring/inspect.d.ts +24 -0
- package/dist/host-wiring/inspect.js +102 -0
- package/dist/host-wiring/mcp.d.ts +55 -0
- package/dist/host-wiring/mcp.js +238 -0
- package/dist/host-wiring/zed.d.ts +10 -0
- package/dist/host-wiring/zed.js +97 -0
- package/dist/host.d.ts +28 -0
- package/dist/host.js +131 -16
- package/package.json +8 -8
package/dist/bin.js
CHANGED
|
@@ -11,9 +11,10 @@ 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
|
-
const VERSION = '0.1.
|
|
17
|
+
const VERSION = '0.1.5';
|
|
17
18
|
async function main() {
|
|
18
19
|
const argv = process.argv.slice(2);
|
|
19
20
|
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
@@ -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;
|
|
@@ -137,7 +142,7 @@ function showHelp() {
|
|
|
137
142
|
Usage: envseal <command> [options]
|
|
138
143
|
|
|
139
144
|
Commands:
|
|
140
|
-
init [--host <name>] Initialize
|
|
145
|
+
init [--host <name>] Initialize manifest, AGENTS.md, and host MCP
|
|
141
146
|
ensure [--check] Prompt for all missing required keys
|
|
142
147
|
(--check: report only, exit 0/1, never prompt)
|
|
143
148
|
set <KEY> Prompt for a single key
|
|
@@ -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:
|
package/dist/cli-utils.js
CHANGED
|
@@ -161,9 +161,9 @@ export function parseArgs(argv) {
|
|
|
161
161
|
const COMMAND_USAGE = {
|
|
162
162
|
init: `Usage: envseal init [--host <name>] [--json] [--project <path>]
|
|
163
163
|
|
|
164
|
-
Initialize env.schema.jsonc,
|
|
164
|
+
Initialize env.schema.jsonc, merge AGENTS.md (Layer 1), and write project host MCP/config for every matching marker.
|
|
165
165
|
|
|
166
|
-
--host <name>
|
|
166
|
+
--host <name> Write this host's project config (comma-separated ok). Valid values: claude-code, cursor, continue, aider, windsurf, cline, zed, codex, jetbrains, goose, copilot, generic, unknown, openhands.
|
|
167
167
|
--json Output as JSON.
|
|
168
168
|
--project <path> Project root (default: auto-detect).`,
|
|
169
169
|
ensure: `Usage: envseal ensure [--check] [--json] [--project <path>]
|
|
@@ -202,8 +202,8 @@ Asks for confirmation first; --yes (or ENVSEAL_ASSUME_YES=1) pre-approves it.
|
|
|
202
202
|
--project <path> Project root (default: auto-detect).`,
|
|
203
203
|
doctor: `Usage: envseal doctor [--json] [--project <path>]
|
|
204
204
|
|
|
205
|
-
Audit the project configuration: detected host and tier,
|
|
206
|
-
file permissions, missing required keys.
|
|
205
|
+
Audit the project configuration: detected host and tier, agent wiring (MCP +
|
|
206
|
+
AGENTS.md), gitignore coverage, file permissions, missing required keys.
|
|
207
207
|
|
|
208
208
|
--json Output as JSON.
|
|
209
209
|
--project <path> Project root (default: auto-detect).`,
|
|
@@ -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,12 +1,13 @@
|
|
|
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, 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';
|
|
8
8
|
import { createBroker } from '../cli-utils.js';
|
|
9
9
|
import { finish } from '../exit.js';
|
|
10
|
+
import { inspectPrimaryHostWiring, wiringFailsDoctor } from '../host-wiring/inspect.js';
|
|
10
11
|
export async function doctor(root, json) {
|
|
11
12
|
try {
|
|
12
13
|
// An audit of a project with no configuration would report an empty,
|
|
@@ -21,6 +22,7 @@ export async function doctor(root, json) {
|
|
|
21
22
|
}));
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
25
|
+
const manifest = loadManifest(projectPaths(root));
|
|
24
26
|
const broker = await createBroker(root);
|
|
25
27
|
const status = await broker.describe();
|
|
26
28
|
const gitignorePath = join(root, '.gitignore');
|
|
@@ -28,13 +30,25 @@ export async function doctor(root, json) {
|
|
|
28
30
|
const gitSafety = inspectDotenvGitSafety(projectPaths(root));
|
|
29
31
|
const gitignoreCovers = gitSafety.ignored;
|
|
30
32
|
const hookFailClosed = process.env.ENVSEAL_HOOK_FAIL_CLOSED === '1';
|
|
31
|
-
// Check .env permissions
|
|
32
|
-
|
|
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;
|
|
33
42
|
if (existsSync(envPath)) {
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
if (process.platform !== 'win32') {
|
|
44
|
+
const stats = statSync(envPath);
|
|
45
|
+
envFileOk = (stats.mode & 0o077) === 0;
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
const host = detectHost(root);
|
|
49
|
+
const egress = manifest?.policy?.egress;
|
|
50
|
+
const inspection = inspectPrimaryHostWiring(root, host.id, { probe: true });
|
|
51
|
+
const hookLastRan = readHookHeartbeat(root);
|
|
38
52
|
const output = {
|
|
39
53
|
projectRoot: root,
|
|
40
54
|
manifestPath,
|
|
@@ -45,6 +59,7 @@ export async function doctor(root, json) {
|
|
|
45
59
|
reason: host.reason,
|
|
46
60
|
recommendation: host.recommendation,
|
|
47
61
|
},
|
|
62
|
+
agentWiring: inspection.wiring,
|
|
48
63
|
gitignore: {
|
|
49
64
|
exists: existsSync(gitignorePath),
|
|
50
65
|
covers: gitignoreCovers,
|
|
@@ -54,29 +69,57 @@ export async function doctor(root, json) {
|
|
|
54
69
|
isTracked: gitSafety.tracked,
|
|
55
70
|
permissionsOk: envFileOk,
|
|
56
71
|
},
|
|
72
|
+
egressPolicy: egress ?? { mode: 'warn', allow: [] },
|
|
57
73
|
hookFailClosed,
|
|
74
|
+
hookLastRan,
|
|
58
75
|
missingRequiredCount: status.missingRequired.length,
|
|
59
76
|
missingRequired: status.missingRequired,
|
|
77
|
+
rotationOverdue: status.entries
|
|
78
|
+
.filter((e) => isOverdue(e.rotationDue))
|
|
79
|
+
.map((e) => ({ key: e.key, due: e.rotationDue })),
|
|
80
|
+
...(inspection.mcp === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: {
|
|
83
|
+
mcp: {
|
|
84
|
+
wired: inspection.mcp.wired,
|
|
85
|
+
status: inspection.mcp.status,
|
|
86
|
+
message: inspection.mcp.message,
|
|
87
|
+
commandOk: inspection.mcp.commandOk,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
60
90
|
};
|
|
61
91
|
if (!json) {
|
|
62
92
|
console.log(`Project root: ${root}`);
|
|
63
93
|
console.log(`Host: ${host.name} (Tier ${host.tier})`);
|
|
64
94
|
console.log(` ${host.reason}`);
|
|
65
95
|
console.log(` ${host.recommendation}`);
|
|
96
|
+
console.log(`Agent wiring: MCP ${inspection.wiring.mcp}, instructions ${inspection.wiring.instructions}`);
|
|
97
|
+
if (inspection.notOotb) {
|
|
98
|
+
console.log(' This host is not OOTB (print-only MCP). Layer 1 AGENTS.md is the working path.');
|
|
99
|
+
}
|
|
100
|
+
console.log(` ${inspection.message}`);
|
|
66
101
|
console.log(`Gitignore covers .env: ${gitignoreCovers ? 'yes' : 'no'}`);
|
|
102
|
+
console.log(`Egress policy: ${egress?.mode === 'allowlist' ? `allowlist (${egress.allow.length} allowed host${egress.allow.length === 1 ? '' : 's'})` : 'warn (default)'}`);
|
|
67
103
|
console.log(`Hook on internal error: ${hookFailClosed ? 'fail-closed' : 'fail-open (default)'}`);
|
|
104
|
+
console.log(`Hook heartbeat: ${describeHeartbeatAge(hookLastRan)}`);
|
|
68
105
|
console.log(`Missing required keys: ${status.missingRequired.length}`);
|
|
69
106
|
if (status.missingRequired.length > 0) {
|
|
70
107
|
for (const key of status.missingRequired) {
|
|
71
108
|
console.log(` - ${key}`);
|
|
72
109
|
}
|
|
73
110
|
}
|
|
111
|
+
const overdue = status.entries.filter((e) => e.rotationDue !== null && isOverdue(e.rotationDue));
|
|
112
|
+
if (overdue.length > 0) {
|
|
113
|
+
console.log('Rotation overdue (advisory — rotate the credential, then rewrite the value):');
|
|
114
|
+
for (const e of overdue) {
|
|
115
|
+
console.log(` - ${e.key}: due ${e.rotationDue.slice(0, 10)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
74
118
|
}
|
|
75
119
|
else {
|
|
76
120
|
emit(json, '', output);
|
|
77
121
|
}
|
|
78
|
-
|
|
79
|
-
if (status.missingRequired.length > 0) {
|
|
122
|
+
if (status.missingRequired.length > 0 || wiringFailsDoctor(inspection)) {
|
|
80
123
|
finish(EXIT.UNSATISFIED);
|
|
81
124
|
return;
|
|
82
125
|
}
|
|
@@ -85,4 +128,38 @@ export async function doctor(root, json) {
|
|
|
85
128
|
fail(json, error);
|
|
86
129
|
}
|
|
87
130
|
}
|
|
131
|
+
/** Advisory by design: overdue rotation never fails doctor the way a
|
|
132
|
+
* missing required key does, because an aged-but-working credential is a
|
|
133
|
+
* hygiene problem, not an outage. */
|
|
134
|
+
function isOverdue(rotationDue) {
|
|
135
|
+
if (rotationDue === null)
|
|
136
|
+
return false;
|
|
137
|
+
const due = Date.parse(rotationDue);
|
|
138
|
+
return !Number.isNaN(due) && due <= Date.now();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Human phrasing for the hook heartbeat. Advisory only — wiring can be
|
|
142
|
+
* present while the hook has never run (no plugin version, no tool call yet),
|
|
143
|
+
* and a recent timestamp proves liveness, not correctness.
|
|
144
|
+
*/
|
|
145
|
+
function describeHeartbeatAge(hookLastRan) {
|
|
146
|
+
if (hookLastRan === null) {
|
|
147
|
+
return 'none recorded (hook has not run for this project, or pre-heartbeat plugin)';
|
|
148
|
+
}
|
|
149
|
+
const then = Date.parse(hookLastRan);
|
|
150
|
+
if (Number.isNaN(then)) {
|
|
151
|
+
return 'unreadable timestamp';
|
|
152
|
+
}
|
|
153
|
+
const seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
|
|
154
|
+
if (seconds < 90)
|
|
155
|
+
return `${seconds}s ago`;
|
|
156
|
+
const minutes = Math.round(seconds / 60);
|
|
157
|
+
if (minutes < 90)
|
|
158
|
+
return `${minutes}m ago`;
|
|
159
|
+
const hours = Math.round(minutes / 60);
|
|
160
|
+
if (hours < 36)
|
|
161
|
+
return `${hours}h ago`;
|
|
162
|
+
const days = Math.round(hours / 24);
|
|
163
|
+
return `${days}d ago`;
|
|
164
|
+
}
|
|
88
165
|
//# sourceMappingURL=doctor.js.map
|
package/dist/commands/init.js
CHANGED
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
import { projectPaths, loadManifest, declareEntries, scanManifestEntry } from '@envseal/core';
|
|
2
2
|
import { SepError } from '@envseal/protocol';
|
|
3
3
|
import { emit, fail } from '../output.js';
|
|
4
|
-
import { detectHost } from '../host.js';
|
|
4
|
+
import { detectHost, resolveInitHostIds } from '../host.js';
|
|
5
5
|
import { scanForEnvKeys, entryForKey } from '../scan.js';
|
|
6
6
|
import { EXIT } from '../exit-codes.js';
|
|
7
7
|
import { finish } from '../exit.js';
|
|
8
|
-
|
|
9
|
-
// silently, recording a host detection would never report and printing a tier
|
|
10
|
-
// computed for a fiction.
|
|
11
|
-
const KNOWN_HOST_IDS = [
|
|
12
|
-
'claude-code',
|
|
13
|
-
'cursor',
|
|
14
|
-
'continue',
|
|
15
|
-
'aider',
|
|
16
|
-
'windsurf',
|
|
17
|
-
'cline',
|
|
18
|
-
'zed',
|
|
19
|
-
'codex',
|
|
20
|
-
'jetbrains',
|
|
21
|
-
'goose',
|
|
22
|
-
'copilot',
|
|
23
|
-
'generic',
|
|
24
|
-
'unknown',
|
|
25
|
-
];
|
|
8
|
+
import { applyHostWiring } from '../host-wiring/apply.js';
|
|
26
9
|
export async function init(root, json, hostOverride) {
|
|
27
10
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
const resolved = resolveInitHostIds(root, hostOverride);
|
|
12
|
+
if (resolved.error !== undefined) {
|
|
13
|
+
console.error(`Error: ${resolved.error}`);
|
|
30
14
|
finish(EXIT.USAGE);
|
|
31
15
|
return;
|
|
32
16
|
}
|
|
@@ -60,13 +44,19 @@ export async function init(root, json, hostOverride) {
|
|
|
60
44
|
// below must be true, not aspirational.
|
|
61
45
|
const result = declareEntries(paths, entries);
|
|
62
46
|
const manifest = loadManifest(paths);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
47
|
+
const wiring = applyHostWiring(root, resolved.ids);
|
|
48
|
+
// Evidence after write: --host cursor on a bare tree now has `.cursor/`.
|
|
49
|
+
// Never invent a fake tier from the flag alone.
|
|
50
|
+
const detected = detectHost(root);
|
|
51
|
+
const cursorEntry = wiring.hosts.find((h) => h.id === 'cursor');
|
|
52
|
+
const cursorWiring = wiring.cursor;
|
|
66
53
|
const output = {
|
|
67
54
|
manifestPath: paths.manifest,
|
|
68
|
-
host:
|
|
69
|
-
protectionTier:
|
|
55
|
+
host: detected.id,
|
|
56
|
+
protectionTier: detected.tier,
|
|
57
|
+
requestedHosts: resolved.source === 'flag' ? resolved.ids : undefined,
|
|
58
|
+
wiredHosts: resolved.ids,
|
|
59
|
+
wiringSource: resolved.source,
|
|
70
60
|
scanned: discovered.length,
|
|
71
61
|
added: result.added,
|
|
72
62
|
updated: result.updated,
|
|
@@ -74,6 +64,25 @@ export async function init(root, json, hostOverride) {
|
|
|
74
64
|
secretKeys: discovered.filter((d) => d.secret).map((d) => d.key),
|
|
75
65
|
configKeys: discovered.filter((d) => !d.secret).map((d) => d.key),
|
|
76
66
|
entries: manifest?.entries.length ?? 0,
|
|
67
|
+
agentsMd: {
|
|
68
|
+
action: wiring.agentsMd.action,
|
|
69
|
+
path: wiring.agentsMd.path,
|
|
70
|
+
},
|
|
71
|
+
hostWiring: wiring.hosts.map((h) => ({
|
|
72
|
+
id: h.id,
|
|
73
|
+
action: h.action,
|
|
74
|
+
path: h.path,
|
|
75
|
+
})),
|
|
76
|
+
...(cursorWiring === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: {
|
|
79
|
+
cursorWiring: {
|
|
80
|
+
mcp: cursorWiring.mcp,
|
|
81
|
+
rules: cursorWiring.rules,
|
|
82
|
+
mcpPath: cursorWiring.mcpPath,
|
|
83
|
+
rulesPath: cursorWiring.rulesPath,
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
77
86
|
};
|
|
78
87
|
if (json) {
|
|
79
88
|
emit(json, '', output);
|
|
@@ -96,23 +105,42 @@ export async function init(root, json, hostOverride) {
|
|
|
96
105
|
console.log(` Config (not prompted): ${config.map((s) => s.key).join(', ')}`);
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
|
-
console.log(`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.log(' Override recorded; envseal doctor reports what is actually detected.');
|
|
108
|
+
console.log(` AGENTS.md: ${wiring.agentsMd.action} (Layer 1 — envseal ensure / envseal run --)`);
|
|
109
|
+
console.log(` Detected host: ${detected.name} (protection tier ${detected.tier})`);
|
|
110
|
+
console.log(` ${detected.reason}`);
|
|
111
|
+
if (detected.recommendation)
|
|
112
|
+
console.log(` ${detected.recommendation}`);
|
|
113
|
+
if (resolved.source === 'flag') {
|
|
114
|
+
console.log(` Requested host(s): ${resolved.ids.join(', ')}. Override recorded; envseal doctor reports what is actually detected.`);
|
|
107
115
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
console.log(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
if (resolved.source === 'none') {
|
|
117
|
+
console.log(' No project host markers and this process is not an IDE.');
|
|
118
|
+
console.log(' Wrote AGENTS.md only. Re-run from the IDE, or `envseal init --host cursor`.');
|
|
119
|
+
}
|
|
120
|
+
else if (resolved.ids.length > 0) {
|
|
121
|
+
console.log(` Wired host(s): ${resolved.ids.join(', ')} (${resolved.source})`);
|
|
122
|
+
}
|
|
123
|
+
for (const entry of wiring.hosts) {
|
|
124
|
+
if (entry.hint) {
|
|
125
|
+
for (const line of entry.hint.split('\n')) {
|
|
126
|
+
console.log(` ${line}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (cursorEntry === undefined && !wiring.bareTerminal) {
|
|
131
|
+
console.log(' Reload MCP / restart the host, then run `envseal doctor`.');
|
|
132
|
+
}
|
|
133
|
+
else if (cursorWiring !== undefined && cursorWiring.mcp !== 'skipped' && wiring.hosts.length === 1) {
|
|
134
|
+
// Cursor entry already printed reloadHint.
|
|
135
|
+
}
|
|
136
|
+
else if (wiring.bareTerminal && resolved.source === 'none') {
|
|
137
|
+
// Already printed the re-run hint.
|
|
138
|
+
}
|
|
139
|
+
else if (wiring.hosts.some((h) => h.id !== 'cursor')) {
|
|
140
|
+
// Per-host hints already cover reload; keep a single closer.
|
|
141
|
+
}
|
|
142
|
+
if (resolved.ids.includes('claude-code')) {
|
|
143
|
+
console.log(' Claude Code: protocol connected (Tier B) via .mcp.json. Plugin = Tier A.');
|
|
116
144
|
}
|
|
117
145
|
}
|
|
118
146
|
catch (error) {
|
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;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1 instruction file shipped by `envseal init`.
|
|
3
|
+
*
|
|
4
|
+
* Keep this string identical to `plugins/generic/AGENTS.md`. The host-wiring
|
|
5
|
+
* test fails if they drift. Embedding (not reading plugins/ at runtime) is
|
|
6
|
+
* what makes published `@envseal/cli` able to write the file.
|
|
7
|
+
*/
|
|
8
|
+
export declare const AGENTS_MD_CONTENT = "# envseal \u2014 instructions for any coding agent\n\nThis repository uses envseal to provision secrets. The secret values must never\nappear in this chat, in your tool output, in files you write, or in commands you\nrun. Follow these rules without exception.\n\n## You must never\n\n- Read, write, or copy the contents of any `.env` or `.env.*` file. (`.env.example`\n is the only exception: it holds placeholders, never values.)\n- Run `printenv`, bare `env`, `export -p`, `set`, or `echo $VAR` / `echo $KEY...`\n to inspect or display environment variables.\n- Use `cat`, `head`, `tail`, `less`, `grep`, `xxd`, `base64`, `sed`, or any other\n reader on a secrets file: `*.pem`, `*.key`, `id_rsa*`, `credentials.json`,\n `secrets.json` / `secrets.yaml` / `secrets.toml`, `.envseal/*`.\n- Ask the user to paste an API key, token, or connection string into the chat.\n- Echo an environment variable into a transcript, log, diff, or tool result.\n\n## How to check which keys exist\n\n```sh\nenvseal status # human-readable\nenvseal status --json # machine-readable; never contains values\n```\n\nStatuses are presence + metadata only (format-valid, length bucket, fingerprint,\nlast verified). There is no way to make envseal print a value.\n\n## How to provision a missing key\n\n```sh\nenvseal ensure # prompts the user for every missing required key\nenvseal set KEY # prompts for a single key\n```\n\nNever ask the user to add a key to `.env` and tell you about it. Run\n`envseal ensure` instead, and wait for its exit code.\n\n## How to run a command that needs secrets\n\n```sh\nenvseal run -- <command...>\n```\n\nSecrets are injected only into that child process and its stdout/stderr are\nredacted. Never run the command plain and never try to read the key yourself.\n\n## How to verify a key actually works\n\n```sh\nenvseal verify # classified results: ok, auth_failed, rate_limited, ...\n```\n\n## How to ask the user what is wrong\n\n```sh\nenvseal doctor # project root, detected host + tier, gitignore, missing keys\n```\n\n## Exit codes (for scripts)\n\n`0` ok \u00B7 `1` required keys missing \u00B7 `2` usage error \u00B7 `3` cancelled \u00B7\n`4` no interactive surface \u00B7 `5` sink failure \u00B7 `6` verification failed.\nUse them; never parse human text for secrets.\n";
|
|
9
|
+
//# sourceMappingURL=agents-md-content.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1 instruction file shipped by `envseal init`.
|
|
3
|
+
*
|
|
4
|
+
* Keep this string identical to `plugins/generic/AGENTS.md`. The host-wiring
|
|
5
|
+
* test fails if they drift. Embedding (not reading plugins/ at runtime) is
|
|
6
|
+
* what makes published `@envseal/cli` able to write the file.
|
|
7
|
+
*/
|
|
8
|
+
export const AGENTS_MD_CONTENT = `# envseal — instructions for any coding agent
|
|
9
|
+
|
|
10
|
+
This repository uses envseal to provision secrets. The secret values must never
|
|
11
|
+
appear in this chat, in your tool output, in files you write, or in commands you
|
|
12
|
+
run. Follow these rules without exception.
|
|
13
|
+
|
|
14
|
+
## You must never
|
|
15
|
+
|
|
16
|
+
- Read, write, or copy the contents of any \`.env\` or \`.env.*\` file. (\`.env.example\`
|
|
17
|
+
is the only exception: it holds placeholders, never values.)
|
|
18
|
+
- Run \`printenv\`, bare \`env\`, \`export -p\`, \`set\`, or \`echo $VAR\` / \`echo $KEY...\`
|
|
19
|
+
to inspect or display environment variables.
|
|
20
|
+
- Use \`cat\`, \`head\`, \`tail\`, \`less\`, \`grep\`, \`xxd\`, \`base64\`, \`sed\`, or any other
|
|
21
|
+
reader on a secrets file: \`*.pem\`, \`*.key\`, \`id_rsa*\`, \`credentials.json\`,
|
|
22
|
+
\`secrets.json\` / \`secrets.yaml\` / \`secrets.toml\`, \`.envseal/*\`.
|
|
23
|
+
- Ask the user to paste an API key, token, or connection string into the chat.
|
|
24
|
+
- Echo an environment variable into a transcript, log, diff, or tool result.
|
|
25
|
+
|
|
26
|
+
## How to check which keys exist
|
|
27
|
+
|
|
28
|
+
\`\`\`sh
|
|
29
|
+
envseal status # human-readable
|
|
30
|
+
envseal status --json # machine-readable; never contains values
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
Statuses are presence + metadata only (format-valid, length bucket, fingerprint,
|
|
34
|
+
last verified). There is no way to make envseal print a value.
|
|
35
|
+
|
|
36
|
+
## How to provision a missing key
|
|
37
|
+
|
|
38
|
+
\`\`\`sh
|
|
39
|
+
envseal ensure # prompts the user for every missing required key
|
|
40
|
+
envseal set KEY # prompts for a single key
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
Never ask the user to add a key to \`.env\` and tell you about it. Run
|
|
44
|
+
\`envseal ensure\` instead, and wait for its exit code.
|
|
45
|
+
|
|
46
|
+
## How to run a command that needs secrets
|
|
47
|
+
|
|
48
|
+
\`\`\`sh
|
|
49
|
+
envseal run -- <command...>
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
Secrets are injected only into that child process and its stdout/stderr are
|
|
53
|
+
redacted. Never run the command plain and never try to read the key yourself.
|
|
54
|
+
|
|
55
|
+
## How to verify a key actually works
|
|
56
|
+
|
|
57
|
+
\`\`\`sh
|
|
58
|
+
envseal verify # classified results: ok, auth_failed, rate_limited, ...
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
## How to ask the user what is wrong
|
|
62
|
+
|
|
63
|
+
\`\`\`sh
|
|
64
|
+
envseal doctor # project root, detected host + tier, gitignore, missing keys
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
## Exit codes (for scripts)
|
|
68
|
+
|
|
69
|
+
\`0\` ok · \`1\` required keys missing · \`2\` usage error · \`3\` cancelled ·
|
|
70
|
+
\`4\` no interactive surface · \`5\` sink failure · \`6\` verification failed.
|
|
71
|
+
Use them; never parse human text for secrets.
|
|
72
|
+
`;
|
|
73
|
+
//# sourceMappingURL=agents-md-content.js.map
|