@envseal/cli 0.1.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/LICENSE +201 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +161 -0
- package/dist/cli-utils.d.ts +50 -0
- package/dist/cli-utils.js +228 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +89 -0
- package/dist/commands/ensure.d.ts +2 -0
- package/dist/commands/ensure.js +126 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +101 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +21 -0
- package/dist/commands/revoke.d.ts +2 -0
- package/dist/commands/revoke.js +48 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +82 -0
- package/dist/commands/set.d.ts +2 -0
- package/dist/commands/set.js +122 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +49 -0
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.js +52 -0
- package/dist/exit-codes.d.ts +24 -0
- package/dist/exit-codes.js +73 -0
- package/dist/exit.d.ts +37 -0
- package/dist/exit.js +66 -0
- package/dist/host.d.ts +10 -0
- package/dist/host.js +286 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/output.d.ts +16 -0
- package/dist/output.js +57 -0
- package/dist/probe-approval.d.ts +10 -0
- package/dist/probe-approval.js +94 -0
- package/dist/scan.d.ts +9 -0
- package/dist/scan.js +138 -0
- package/dist/test-prompter.d.ts +33 -0
- package/dist/test-prompter.js +61 -0
- package/package.json +39 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { projectPaths } from '@envseal/core';
|
|
2
|
+
import { emit, fail } from '../output.js';
|
|
3
|
+
import { EXIT } from '../exit-codes.js';
|
|
4
|
+
import { createBroker } from '../cli-utils.js';
|
|
5
|
+
import { finish } from '../exit.js';
|
|
6
|
+
import { makeProbeApprover } from '../probe-approval.js';
|
|
7
|
+
export async function verify(root, keys, json) {
|
|
8
|
+
try {
|
|
9
|
+
const paths = projectPaths(root);
|
|
10
|
+
const broker = await createBroker(root, {
|
|
11
|
+
onApprovalNeeded: makeProbeApprover(paths.approvals),
|
|
12
|
+
});
|
|
13
|
+
const status = await broker.describe();
|
|
14
|
+
// Verify specified keys, or all if none specified
|
|
15
|
+
const keysToVerify = keys.length > 0
|
|
16
|
+
? keys
|
|
17
|
+
: status.entries.map((e) => e.key);
|
|
18
|
+
const results = await broker.verify({
|
|
19
|
+
keys: keysToVerify,
|
|
20
|
+
});
|
|
21
|
+
let allOk = true;
|
|
22
|
+
for (const result of results) {
|
|
23
|
+
if (result.result !== 'ok') {
|
|
24
|
+
allOk = false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!json) {
|
|
28
|
+
for (const result of results) {
|
|
29
|
+
const status_str = result.result === 'ok' ? '✓' : '✗';
|
|
30
|
+
console.log(`${status_str} ${result.key}: ${result.result}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
emit(json, '', {
|
|
35
|
+
results: results.map((r) => ({
|
|
36
|
+
key: r.key,
|
|
37
|
+
result: r.result,
|
|
38
|
+
message: r.message,
|
|
39
|
+
})),
|
|
40
|
+
allOk,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (!allOk) {
|
|
44
|
+
finish(EXIT.VERIFY_FAILED);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
fail(json, error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type TicketKeyOutcome } from '@envseal/protocol';
|
|
2
|
+
export declare const EXIT: {
|
|
3
|
+
readonly OK: 0;
|
|
4
|
+
readonly UNSATISFIED: 1;
|
|
5
|
+
readonly USAGE: 2;
|
|
6
|
+
readonly CANCELLED: 3;
|
|
7
|
+
readonly NO_SURFACE: 4;
|
|
8
|
+
readonly SINK_FAILURE: 5;
|
|
9
|
+
readonly VERIFY_FAILED: 6;
|
|
10
|
+
};
|
|
11
|
+
export declare function exitCodeForError(e: unknown): number;
|
|
12
|
+
/**
|
|
13
|
+
* Exit code for a per-key ticket outcome, per docs/cli-contract.md.
|
|
14
|
+
*
|
|
15
|
+
* `set` and `ensure` previously exited 0 for every outcome including
|
|
16
|
+
* `cancelled`, `invalid_format` and `timeout`, so a shell caller could not tell
|
|
17
|
+
* a stored key from a refused one.
|
|
18
|
+
*
|
|
19
|
+
* `timeout` maps to CANCELLED rather than UNSATISFIED so it agrees with
|
|
20
|
+
* exitCodeForError, which already maps SEP_TICKET_EXPIRED to CANCELLED. The
|
|
21
|
+
* same event reaching a caller by two routes must not produce two codes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function exitCodeForOutcome(outcome: TicketKeyOutcome): number;
|
|
24
|
+
//# sourceMappingURL=exit-codes.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isSepError } from '@envseal/protocol';
|
|
2
|
+
export const EXIT = {
|
|
3
|
+
OK: 0,
|
|
4
|
+
UNSATISFIED: 1,
|
|
5
|
+
USAGE: 2,
|
|
6
|
+
CANCELLED: 3,
|
|
7
|
+
NO_SURFACE: 4,
|
|
8
|
+
SINK_FAILURE: 5,
|
|
9
|
+
VERIFY_FAILED: 6,
|
|
10
|
+
};
|
|
11
|
+
export function exitCodeForError(e) {
|
|
12
|
+
if (!isSepError(e)) {
|
|
13
|
+
// Anything that is not a SepError is still a failure. Returning OK here
|
|
14
|
+
// meant a generic thrown Error could exit 0 — a success code on a path that
|
|
15
|
+
// only runs because something went wrong.
|
|
16
|
+
return EXIT.UNSATISFIED;
|
|
17
|
+
}
|
|
18
|
+
const code = e.code;
|
|
19
|
+
switch (code) {
|
|
20
|
+
case 'SEP_UNKNOWN_KEY':
|
|
21
|
+
case 'SEP_NOT_DECLARED':
|
|
22
|
+
case 'SEP_GITIGNORE_UNSAFE':
|
|
23
|
+
case 'SEP_PROBE_NOT_APPROVED':
|
|
24
|
+
case 'SEP_VALUE_IN_REQUEST':
|
|
25
|
+
return EXIT.USAGE;
|
|
26
|
+
case 'SEP_NO_INTERACTIVE_SURFACE':
|
|
27
|
+
return EXIT.NO_SURFACE;
|
|
28
|
+
case 'SEP_USER_CANCELLED':
|
|
29
|
+
case 'SEP_TICKET_EXPIRED':
|
|
30
|
+
return EXIT.CANCELLED;
|
|
31
|
+
case 'SEP_SINK_UNAVAILABLE':
|
|
32
|
+
case 'SEP_SINK_WRITE_FAILED':
|
|
33
|
+
return EXIT.SINK_FAILURE;
|
|
34
|
+
case 'SEP_FORMAT_INVALID':
|
|
35
|
+
case 'SEP_RATE_LIMITED':
|
|
36
|
+
case 'SEP_TICKET_UNKNOWN':
|
|
37
|
+
case 'SEP_CONFIRMATION_DENIED':
|
|
38
|
+
return EXIT.UNSATISFIED;
|
|
39
|
+
default: {
|
|
40
|
+
const _exhaustive = code;
|
|
41
|
+
return _exhaustive;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Exit code for a per-key ticket outcome, per docs/cli-contract.md.
|
|
47
|
+
*
|
|
48
|
+
* `set` and `ensure` previously exited 0 for every outcome including
|
|
49
|
+
* `cancelled`, `invalid_format` and `timeout`, so a shell caller could not tell
|
|
50
|
+
* a stored key from a refused one.
|
|
51
|
+
*
|
|
52
|
+
* `timeout` maps to CANCELLED rather than UNSATISFIED so it agrees with
|
|
53
|
+
* exitCodeForError, which already maps SEP_TICKET_EXPIRED to CANCELLED. The
|
|
54
|
+
* same event reaching a caller by two routes must not produce two codes.
|
|
55
|
+
*/
|
|
56
|
+
export function exitCodeForOutcome(outcome) {
|
|
57
|
+
switch (outcome) {
|
|
58
|
+
case 'stored':
|
|
59
|
+
return EXIT.OK;
|
|
60
|
+
case 'cancelled':
|
|
61
|
+
case 'timeout':
|
|
62
|
+
return EXIT.CANCELLED;
|
|
63
|
+
case 'skipped':
|
|
64
|
+
case 'invalid_format':
|
|
65
|
+
case 'verify_failed':
|
|
66
|
+
return EXIT.UNSATISFIED;
|
|
67
|
+
default: {
|
|
68
|
+
const _exhaustive = outcome;
|
|
69
|
+
return _exhaustive;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=exit-codes.js.map
|
package/dist/exit.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process termination for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* `process.exit()` is the wrong tool here and was the cause of launch blocker
|
|
5
|
+
* B4. Two independent problems:
|
|
6
|
+
*
|
|
7
|
+
* 1. On Windows, calling `process.exit()` while undici is tearing down the
|
|
8
|
+
* sockets a completed `fetch()` left behind trips a libuv assertion —
|
|
9
|
+
* `!(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76` —
|
|
10
|
+
* and the process dies with 0xC0000409 (3221226505). An agent branching on
|
|
11
|
+
* the exit code sees a crash instead of the documented 6. Reproduced 4/5
|
|
12
|
+
* runs before this change; see scripts/probe-b4-repro.mjs. Flushing stdout
|
|
13
|
+
* first does NOT help: the assertion is about the fetch handles, not stdout.
|
|
14
|
+
* 2. `process.exit()` truncates a pending stdout write to a pipe, which is
|
|
15
|
+
* exactly what a shell-only caller reads from.
|
|
16
|
+
*
|
|
17
|
+
* So the normal path never calls `process.exit()`. It releases everything the
|
|
18
|
+
* CLI owns, sets `process.exitCode`, and lets the event loop drain — which is
|
|
19
|
+
* also the only way Node guarantees stdout is flushed. An *unref'd* watchdog
|
|
20
|
+
* covers the case where a handle we do not own keeps the loop alive: being
|
|
21
|
+
* unref'd it cannot delay the normal path, and it only fires if the process
|
|
22
|
+
* would otherwise hang.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Register a teardown callback to run before the process finishes.
|
|
26
|
+
* Every broker the CLI creates registers its `dispose()` here, which clears the
|
|
27
|
+
* ticket store's sweep interval and drops its waiters.
|
|
28
|
+
*/
|
|
29
|
+
export declare function registerDisposable(dispose: () => void): void;
|
|
30
|
+
/**
|
|
31
|
+
* Finish the process with `code`.
|
|
32
|
+
*
|
|
33
|
+
* Returns normally — it is NOT `never`. Callers must `return` after calling it,
|
|
34
|
+
* because execution continues until the current call stack unwinds.
|
|
35
|
+
*/
|
|
36
|
+
export declare function finish(code: number): void;
|
|
37
|
+
//# sourceMappingURL=exit.d.ts.map
|
package/dist/exit.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process termination for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* `process.exit()` is the wrong tool here and was the cause of launch blocker
|
|
5
|
+
* B4. Two independent problems:
|
|
6
|
+
*
|
|
7
|
+
* 1. On Windows, calling `process.exit()` while undici is tearing down the
|
|
8
|
+
* sockets a completed `fetch()` left behind trips a libuv assertion —
|
|
9
|
+
* `!(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76` —
|
|
10
|
+
* and the process dies with 0xC0000409 (3221226505). An agent branching on
|
|
11
|
+
* the exit code sees a crash instead of the documented 6. Reproduced 4/5
|
|
12
|
+
* runs before this change; see scripts/probe-b4-repro.mjs. Flushing stdout
|
|
13
|
+
* first does NOT help: the assertion is about the fetch handles, not stdout.
|
|
14
|
+
* 2. `process.exit()` truncates a pending stdout write to a pipe, which is
|
|
15
|
+
* exactly what a shell-only caller reads from.
|
|
16
|
+
*
|
|
17
|
+
* So the normal path never calls `process.exit()`. It releases everything the
|
|
18
|
+
* CLI owns, sets `process.exitCode`, and lets the event loop drain — which is
|
|
19
|
+
* also the only way Node guarantees stdout is flushed. An *unref'd* watchdog
|
|
20
|
+
* covers the case where a handle we do not own keeps the loop alive: being
|
|
21
|
+
* unref'd it cannot delay the normal path, and it only fires if the process
|
|
22
|
+
* would otherwise hang.
|
|
23
|
+
*/
|
|
24
|
+
/** How long the loop may stay alive after we are done before we force the issue. */
|
|
25
|
+
const HANG_GRACE_MS = 1500;
|
|
26
|
+
/** How long a forced exit waits for stdout to reach the pipe. */
|
|
27
|
+
const FLUSH_GRACE_MS = 250;
|
|
28
|
+
const disposables = new Set();
|
|
29
|
+
/**
|
|
30
|
+
* Register a teardown callback to run before the process finishes.
|
|
31
|
+
* Every broker the CLI creates registers its `dispose()` here, which clears the
|
|
32
|
+
* ticket store's sweep interval and drops its waiters.
|
|
33
|
+
*/
|
|
34
|
+
export function registerDisposable(dispose) {
|
|
35
|
+
disposables.add(dispose);
|
|
36
|
+
}
|
|
37
|
+
function disposeAll() {
|
|
38
|
+
for (const dispose of disposables) {
|
|
39
|
+
try {
|
|
40
|
+
dispose();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Teardown must never mask or change the outcome we are reporting.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
disposables.clear();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Finish the process with `code`.
|
|
50
|
+
*
|
|
51
|
+
* Returns normally — it is NOT `never`. Callers must `return` after calling it,
|
|
52
|
+
* because execution continues until the current call stack unwinds.
|
|
53
|
+
*/
|
|
54
|
+
export function finish(code) {
|
|
55
|
+
disposeAll();
|
|
56
|
+
process.exitCode = code;
|
|
57
|
+
const watchdog = setTimeout(() => {
|
|
58
|
+
// Something outside the CLI's ownership is still holding the event loop
|
|
59
|
+
// open. Forcing the exit is the lesser evil against a command that never
|
|
60
|
+
// returns, but give the pending stdout write a chance to land first.
|
|
61
|
+
process.stdout.write('', () => process.exit(code));
|
|
62
|
+
setTimeout(() => process.exit(code), FLUSH_GRACE_MS).unref();
|
|
63
|
+
}, HANG_GRACE_MS);
|
|
64
|
+
watchdog.unref();
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=exit.js.map
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ProtectionTier = 'A' | 'B' | 'C';
|
|
2
|
+
export interface HostInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
tier: ProtectionTier;
|
|
6
|
+
reason: string;
|
|
7
|
+
recommendation: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function detectHost(root: string): HostInfo;
|
|
10
|
+
//# sourceMappingURL=host.d.ts.map
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
/**
|
|
5
|
+
* Detect the host environment from marker files and env vars.
|
|
6
|
+
* Tier A: protocol + interception hooks (Claude Code)
|
|
7
|
+
* Tier B: protocol + advisory guardrails (Cursor, Continue, Windsurf, Cline,
|
|
8
|
+
* Zed, Codex, JetBrains, Copilot, generic)
|
|
9
|
+
* Tier C: protocol only (Aider, Goose, unknown)
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Are envseal's interception hooks actually wired into Claude Code?
|
|
13
|
+
*
|
|
14
|
+
* Deliberately evidence-based rather than optimistic. We look for envseal named
|
|
15
|
+
* in a settings file's hook configuration, or an installed plugin directory. If
|
|
16
|
+
* we cannot see the wiring we do not claim it — an unfounded tier A tells the
|
|
17
|
+
* user that `cat .env` is blocked when it is not, and they will act accordingly.
|
|
18
|
+
*/
|
|
19
|
+
function envsealHooksInstalled(root) {
|
|
20
|
+
const candidates = [
|
|
21
|
+
join(root, '.claude', 'settings.json'),
|
|
22
|
+
join(root, '.claude', 'settings.local.json'),
|
|
23
|
+
join(homedir(), '.claude', 'settings.json'),
|
|
24
|
+
];
|
|
25
|
+
for (const file of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
const text = readFileSync(file, 'utf8');
|
|
28
|
+
// The hook commands reference the plugin by name whichever way it was
|
|
29
|
+
// installed (marketplace, path, or a hand-written hooks block).
|
|
30
|
+
if (/envseal/i.test(text) && /hooks/i.test(text))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Unreadable or absent: absence of evidence is not evidence of protection.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// A locally installed plugin directory also counts.
|
|
38
|
+
for (const dir of [
|
|
39
|
+
join(root, '.claude', 'plugins', 'envseal'),
|
|
40
|
+
join(homedir(), '.claude', 'plugins', 'envseal'),
|
|
41
|
+
]) {
|
|
42
|
+
if (existsSync(dir))
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Detection runs in TWO passes with a hard precedence rule:
|
|
49
|
+
*
|
|
50
|
+
* Pass 1 — PROJECT-LOCAL evidence only (.claude/, .cursor/, aider.conf.yml, ...).
|
|
51
|
+
* Pass 2 — ENVIRONMENT and GLOBAL-HOME evidence (CLAUDECODE, ~/.codex,
|
|
52
|
+
* ~/.cline, ~/.config/zed, ...) runs ONLY when pass 1 found nothing.
|
|
53
|
+
*
|
|
54
|
+
* A machine where the developer happens to have Codex installed globally
|
|
55
|
+
* (~/.codex exists) used to make EVERY bare directory report "Codex CLI, tier
|
|
56
|
+
* B" — advice about interception and sinks for a project that has nothing to
|
|
57
|
+
* do with that tool. A marker inside the project says something about THIS
|
|
58
|
+
* project; a marker in $HOME only says something about the MACHINE, so it must
|
|
59
|
+
* never outrank or substitute for project evidence.
|
|
60
|
+
*/
|
|
61
|
+
const TIER_B_ADVICE = 'Tier B host with protocol + advisory guardrails only. Shell-command leaks are possible; prefer the keychain sink so .env holds only references.';
|
|
62
|
+
const TIER_C_ADVICE = 'Tier C host with protocol only. No interception hooks available; prefer the keychain sink so .env holds only references.';
|
|
63
|
+
export function detectHost(root) {
|
|
64
|
+
/* ---------------- Pass 1: project-local evidence ---------------- */
|
|
65
|
+
// Claude Code. Detecting the HOST is not the same as detecting the PROTECTION:
|
|
66
|
+
// tier A is earned by envseal's interception hooks actually being installed, and
|
|
67
|
+
// `CLAUDECODE` being set only says which harness is running. Claiming "secrets
|
|
68
|
+
// are maximally protected" on the strength of an environment variable is the
|
|
69
|
+
// precise dishonesty this tier system exists to prevent — a user who believes
|
|
70
|
+
// the hooks are guarding them behaves as if `cat .env` is blocked when it is not.
|
|
71
|
+
if (existsSync(join(root, '.claude'))) {
|
|
72
|
+
if (envsealHooksInstalled(root)) {
|
|
73
|
+
return {
|
|
74
|
+
id: 'claude-code',
|
|
75
|
+
name: 'Claude Code',
|
|
76
|
+
tier: 'A',
|
|
77
|
+
reason: 'Claude Code detected and envseal interception hooks are installed',
|
|
78
|
+
recommendation: 'Tier A: the protocol plus interception hooks. Reads of .env and env-dumping commands are blocked, and pasted keys are redacted before the model sees them.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
id: 'claude-code',
|
|
83
|
+
name: 'Claude Code',
|
|
84
|
+
tier: 'B',
|
|
85
|
+
reason: 'Claude Code detected, but envseal interception hooks were not found in .claude/settings.json or ~/.claude/settings.json',
|
|
86
|
+
recommendation: 'Tier B until the plugin is installed: the protocol works, but nothing blocks a shell command from reading .env. Install the plugin in plugins/claude-code for tier A, or prefer the keychain sink so .env holds only references.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (existsSync(join(root, '.cursor'))) {
|
|
90
|
+
return {
|
|
91
|
+
id: 'cursor',
|
|
92
|
+
name: 'Cursor',
|
|
93
|
+
tier: 'B',
|
|
94
|
+
reason: 'Found .cursor/ directory',
|
|
95
|
+
recommendation: TIER_B_ADVICE,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (existsSync(join(root, '.continue'))) {
|
|
99
|
+
return {
|
|
100
|
+
id: 'continue',
|
|
101
|
+
name: 'Continue',
|
|
102
|
+
tier: 'B',
|
|
103
|
+
reason: 'Found .continue/ directory',
|
|
104
|
+
recommendation: TIER_B_ADVICE,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Tier C: Aider (project-local config forms).
|
|
108
|
+
const aiderConfPatterns = ['aider', '.aider'];
|
|
109
|
+
const aiderMarkerExists = aiderConfPatterns.some((pattern) => existsSync(join(root, `${pattern}.conf.yml`)) ||
|
|
110
|
+
existsSync(join(root, `${pattern}.conf.yaml`)) ||
|
|
111
|
+
existsSync(join(root, `${pattern}.conf.json`)));
|
|
112
|
+
if (aiderMarkerExists) {
|
|
113
|
+
return {
|
|
114
|
+
id: 'aider',
|
|
115
|
+
name: 'Aider',
|
|
116
|
+
tier: 'C',
|
|
117
|
+
reason: 'Found .aider configuration file',
|
|
118
|
+
recommendation: TIER_C_ADVICE,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (existsSync(join(root, '.windsurf'))) {
|
|
122
|
+
return {
|
|
123
|
+
id: 'windsurf',
|
|
124
|
+
name: 'Windsurf',
|
|
125
|
+
tier: 'B',
|
|
126
|
+
reason: 'Found .windsurf/ directory',
|
|
127
|
+
recommendation: TIER_B_ADVICE,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (existsSync(join(root, '.cline'))) {
|
|
131
|
+
return {
|
|
132
|
+
id: 'cline',
|
|
133
|
+
name: 'Cline',
|
|
134
|
+
tier: 'B',
|
|
135
|
+
reason: 'Found .cline/ directory',
|
|
136
|
+
recommendation: TIER_B_ADVICE,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (existsSync(join(root, '.zed'))) {
|
|
140
|
+
return {
|
|
141
|
+
id: 'zed',
|
|
142
|
+
name: 'Zed',
|
|
143
|
+
tier: 'B',
|
|
144
|
+
reason: 'Found .zed/ directory',
|
|
145
|
+
recommendation: TIER_B_ADVICE,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (existsSync(join(root, '.codex'))) {
|
|
149
|
+
return {
|
|
150
|
+
id: 'codex',
|
|
151
|
+
name: 'Codex CLI',
|
|
152
|
+
tier: 'B',
|
|
153
|
+
reason: 'Found .codex/ directory',
|
|
154
|
+
recommendation: TIER_B_ADVICE,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Tier B: JetBrains IDEs (IntelliJ, PyCharm, ...).
|
|
158
|
+
if (existsSync(join(root, '.idea'))) {
|
|
159
|
+
return {
|
|
160
|
+
id: 'jetbrains',
|
|
161
|
+
name: 'JetBrains IDE',
|
|
162
|
+
tier: 'B',
|
|
163
|
+
reason: 'Found .idea/ directory',
|
|
164
|
+
recommendation: TIER_B_ADVICE,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Tier C: Goose (project-local forms).
|
|
168
|
+
if (existsSync(join(root, 'goose.config.yaml')) || existsSync(join(root, '.goose'))) {
|
|
169
|
+
return {
|
|
170
|
+
id: 'goose',
|
|
171
|
+
name: 'Goose',
|
|
172
|
+
tier: 'C',
|
|
173
|
+
reason: 'Found goose.config.yaml or .goose/ directory',
|
|
174
|
+
recommendation: TIER_C_ADVICE,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// Tier B: GitHub Copilot agent. VS Code Copilot has no unique project
|
|
178
|
+
// directory, so the honest marker is Copilot settings inside
|
|
179
|
+
// .vscode/settings.json — a bare .vscode/ proves nothing, every VS Code
|
|
180
|
+
// project has one.
|
|
181
|
+
const vscodeSettings = join(root, '.vscode', 'settings.json');
|
|
182
|
+
let copilotSettings = false;
|
|
183
|
+
try {
|
|
184
|
+
copilotSettings =
|
|
185
|
+
existsSync(vscodeSettings) && /copilot/i.test(readFileSync(vscodeSettings, 'utf8'));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Unreadable settings are not evidence of Copilot.
|
|
189
|
+
}
|
|
190
|
+
if (copilotSettings) {
|
|
191
|
+
return {
|
|
192
|
+
id: 'copilot',
|
|
193
|
+
name: 'GitHub Copilot',
|
|
194
|
+
tier: 'B',
|
|
195
|
+
reason: 'Found .vscode/settings.json referencing Copilot',
|
|
196
|
+
recommendation: TIER_B_ADVICE,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Tier B: Generic (AGENTS.md alone).
|
|
200
|
+
if (existsSync(join(root, 'AGENTS.md'))) {
|
|
201
|
+
return {
|
|
202
|
+
id: 'generic',
|
|
203
|
+
name: 'Generic Agent',
|
|
204
|
+
tier: 'B',
|
|
205
|
+
reason: 'Found AGENTS.md file',
|
|
206
|
+
recommendation: TIER_B_ADVICE,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/* ------------- Pass 2: environment / global-home evidence -------------
|
|
210
|
+
* Reached ONLY when the project carries no marker of its own. */
|
|
211
|
+
if (process.env.CLAUDECODE) {
|
|
212
|
+
return {
|
|
213
|
+
id: 'claude-code',
|
|
214
|
+
name: 'Claude Code',
|
|
215
|
+
tier: 'B',
|
|
216
|
+
reason: 'CLAUDECODE is set (running under Claude Code), no project markers found',
|
|
217
|
+
recommendation: 'Tier B until the plugin is installed: the protocol works, but nothing blocks a shell command from reading .env. Install the plugin in plugins/claude-code for tier A, or prefer the keychain sink so .env holds only references.',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (process.env.CURSOR_WORKSPACE || process.env.CURSOR_VERSION) {
|
|
221
|
+
return {
|
|
222
|
+
id: 'cursor',
|
|
223
|
+
name: 'Cursor',
|
|
224
|
+
tier: 'B',
|
|
225
|
+
reason: 'CURSOR_* environment variable set, no project markers found',
|
|
226
|
+
recommendation: TIER_B_ADVICE,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (process.env.CLINE_ROOT) {
|
|
230
|
+
return {
|
|
231
|
+
id: 'cline',
|
|
232
|
+
name: 'Cline',
|
|
233
|
+
tier: 'B',
|
|
234
|
+
reason: 'CLINE_ROOT is set, no project markers found',
|
|
235
|
+
recommendation: TIER_B_ADVICE,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (process.env.ZED_EDITOR) {
|
|
239
|
+
return {
|
|
240
|
+
id: 'zed',
|
|
241
|
+
name: 'Zed',
|
|
242
|
+
tier: 'B',
|
|
243
|
+
reason: 'ZED_EDITOR is set, no project markers found',
|
|
244
|
+
recommendation: TIER_B_ADVICE,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (process.env.CODEX_ROOT) {
|
|
248
|
+
return {
|
|
249
|
+
id: 'codex',
|
|
250
|
+
name: 'Codex CLI',
|
|
251
|
+
tier: 'B',
|
|
252
|
+
reason: 'CODEX_ROOT is set, no project markers found',
|
|
253
|
+
recommendation: TIER_B_ADVICE,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (existsSync(join(homedir(), '.codeium', 'windsurf')) ||
|
|
257
|
+
existsSync(join(homedir(), '.cline')) ||
|
|
258
|
+
existsSync(join(homedir(), '.config', 'zed')) ||
|
|
259
|
+
existsSync(join(homedir(), '.zed'))) {
|
|
260
|
+
return {
|
|
261
|
+
id: 'generic',
|
|
262
|
+
name: 'Generic Agent',
|
|
263
|
+
tier: 'B',
|
|
264
|
+
reason: 'No project markers found; a globally installed coding agent (Windsurf/Cline/Zed config in $HOME) is present',
|
|
265
|
+
recommendation: 'Advisory tier: the global install suggests an agent MAY act here, but nothing ties it to this project. The protocol still works; prefer the keychain sink so .env holds only references.',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (process.env.GOOSE_ROOT || existsSync(join(homedir(), '.config', 'goose'))) {
|
|
269
|
+
return {
|
|
270
|
+
id: 'goose',
|
|
271
|
+
name: 'Goose',
|
|
272
|
+
tier: 'C',
|
|
273
|
+
reason: 'GOOSE_ROOT is set or global Goose config exists, no project markers found',
|
|
274
|
+
recommendation: TIER_C_ADVICE,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Unknown.
|
|
278
|
+
return {
|
|
279
|
+
id: 'unknown',
|
|
280
|
+
name: 'Unknown Host',
|
|
281
|
+
tier: 'C',
|
|
282
|
+
reason: 'Could not detect a known host environment',
|
|
283
|
+
recommendation: TIER_C_ADVICE,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=host.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { EXIT, exitCodeForError, exitCodeForOutcome } from './exit-codes.js';
|
|
2
|
+
export { emit, fail } from './output.js';
|
|
3
|
+
export { finish, registerDisposable } from './exit.js';
|
|
4
|
+
export type { ProtectionTier, HostInfo } from './host.js';
|
|
5
|
+
export { detectHost } from './host.js';
|
|
6
|
+
export { parseArgs, createBroker, outcomeForKey, hasInteractiveSurface } from './cli-utils.js';
|
|
7
|
+
export { makeProbeApprover } from './probe-approval.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { EXIT, exitCodeForError, exitCodeForOutcome } from './exit-codes.js';
|
|
2
|
+
export { emit, fail } from './output.js';
|
|
3
|
+
export { finish, registerDisposable } from './exit.js';
|
|
4
|
+
export { detectHost } from './host.js';
|
|
5
|
+
export { parseArgs, createBroker, outcomeForKey, hasInteractiveSurface } from './cli-utils.js';
|
|
6
|
+
export { makeProbeApprover } from './probe-approval.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit output in JSON or human-readable format.
|
|
3
|
+
* When json is true, prints ONLY a single JSON object to stdout.
|
|
4
|
+
* When false, prints the human string.
|
|
5
|
+
*/
|
|
6
|
+
export declare function emit(json: boolean, human: string, data: unknown): void;
|
|
7
|
+
/**
|
|
8
|
+
* Print an error and set the appropriate exit code.
|
|
9
|
+
* Never prints a stack trace or a secret value.
|
|
10
|
+
*
|
|
11
|
+
* This does NOT return `never` any more: termination is deferred so the event
|
|
12
|
+
* loop can drain (see exit.ts). Every caller must be the last statement in its
|
|
13
|
+
* catch block, or must `return` immediately after.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fail(json: boolean, error: unknown): void;
|
|
16
|
+
//# sourceMappingURL=output.d.ts.map
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isSepError } from '@envseal/protocol';
|
|
2
|
+
import { EXIT, exitCodeForError } from './exit-codes.js';
|
|
3
|
+
import { finish } from './exit.js';
|
|
4
|
+
/**
|
|
5
|
+
* Emit output in JSON or human-readable format.
|
|
6
|
+
* When json is true, prints ONLY a single JSON object to stdout.
|
|
7
|
+
* When false, prints the human string.
|
|
8
|
+
*/
|
|
9
|
+
export function emit(json, human, data) {
|
|
10
|
+
if (json) {
|
|
11
|
+
console.log(JSON.stringify(data, null, 0));
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log(human);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Print an error and set the appropriate exit code.
|
|
19
|
+
* Never prints a stack trace or a secret value.
|
|
20
|
+
*
|
|
21
|
+
* This does NOT return `never` any more: termination is deferred so the event
|
|
22
|
+
* loop can drain (see exit.ts). Every caller must be the last statement in its
|
|
23
|
+
* catch block, or must `return` immediately after.
|
|
24
|
+
*/
|
|
25
|
+
export function fail(json, error) {
|
|
26
|
+
// A failure path must never default to "OK". The previous initialiser was 0,
|
|
27
|
+
// so throwing anything that was not a SepError, an Error, or a string exited
|
|
28
|
+
// the process successfully while printing an error message.
|
|
29
|
+
let code = EXIT.UNSATISFIED;
|
|
30
|
+
let userMessage = 'An unexpected error occurred.';
|
|
31
|
+
let retriable = false;
|
|
32
|
+
let errorCode;
|
|
33
|
+
if (isSepError(error)) {
|
|
34
|
+
code = exitCodeForError(error);
|
|
35
|
+
userMessage = error.userMessage;
|
|
36
|
+
retriable = error.retriable;
|
|
37
|
+
errorCode = error.code;
|
|
38
|
+
}
|
|
39
|
+
else if (error instanceof Error) {
|
|
40
|
+
userMessage = error.message;
|
|
41
|
+
}
|
|
42
|
+
else if (typeof error === 'string') {
|
|
43
|
+
userMessage = error;
|
|
44
|
+
}
|
|
45
|
+
if (json) {
|
|
46
|
+
console.log(JSON.stringify({
|
|
47
|
+
code: errorCode ?? 'UNKNOWN',
|
|
48
|
+
userMessage,
|
|
49
|
+
retriable,
|
|
50
|
+
}, null, 0));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error(`Error: ${userMessage}`);
|
|
54
|
+
}
|
|
55
|
+
finish(code);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ManifestEntry } from '@envseal/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* Build the `onApprovalNeeded` callback for the broker.
|
|
4
|
+
*
|
|
5
|
+
* Fails closed: with no interactive surface it returns false rather than
|
|
6
|
+
* hanging on a read nobody will answer, and rather than approving silently.
|
|
7
|
+
* Core turns that false into `probe_not_approved`, so `verify` still exits 6.
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeProbeApprover(approvalsPath: string): (entry: ManifestEntry) => Promise<boolean>;
|
|
10
|
+
//# sourceMappingURL=probe-approval.d.ts.map
|