sigild 0.0.1 → 0.0.3
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/README.md +142 -33
- package/THREAT_MODEL.md +47 -19
- package/dist/src/bin/sigil-hook-post.d.ts +3 -0
- package/dist/src/bin/sigil-hook-post.d.ts.map +1 -0
- package/dist/src/bin/sigil-hook-post.js +15 -0
- package/dist/src/bin/sigil-hook-post.js.map +1 -0
- package/dist/src/bin/sigil-hook-pre.d.ts +3 -0
- package/dist/src/bin/sigil-hook-pre.d.ts.map +1 -0
- package/dist/src/bin/sigil-hook-pre.js +18 -0
- package/dist/src/bin/sigil-hook-pre.js.map +1 -0
- package/dist/src/bin/sigil-mcp.d.ts +3 -0
- package/dist/src/bin/sigil-mcp.d.ts.map +1 -0
- package/dist/src/bin/sigil-mcp.js +90 -0
- package/dist/src/bin/sigil-mcp.js.map +1 -0
- package/dist/src/bin/sigil.d.ts +3 -0
- package/dist/src/bin/sigil.d.ts.map +1 -0
- package/dist/src/bin/sigil.js +9 -0
- package/dist/src/bin/sigil.js.map +1 -0
- package/dist/src/cli/args.d.ts +26 -0
- package/dist/src/cli/args.d.ts.map +1 -0
- package/dist/src/cli/args.js +36 -0
- package/dist/src/cli/args.js.map +1 -0
- package/dist/src/cli/index.d.ts +7 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +7 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/main.d.ts +26 -0
- package/dist/src/cli/main.d.ts.map +1 -0
- package/dist/src/cli/main.js +197 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/paths.d.ts +18 -0
- package/dist/src/cli/paths.d.ts.map +1 -0
- package/dist/src/cli/paths.js +13 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/portal.d.ts +59 -0
- package/dist/src/cli/portal.d.ts.map +1 -0
- package/dist/src/cli/portal.js +112 -0
- package/dist/src/cli/portal.js.map +1 -0
- package/dist/src/cli/status.d.ts +28 -0
- package/dist/src/cli/status.d.ts.map +1 -0
- package/dist/src/cli/status.js +59 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/unlock.d.ts +36 -0
- package/dist/src/cli/unlock.d.ts.map +1 -0
- package/dist/src/cli/unlock.js +77 -0
- package/dist/src/cli/unlock.js.map +1 -0
- package/dist/src/control/client.d.ts +26 -0
- package/dist/src/control/client.d.ts.map +1 -0
- package/dist/src/control/client.js +76 -0
- package/dist/src/control/client.js.map +1 -0
- package/dist/src/control/index.d.ts +4 -0
- package/dist/src/control/index.d.ts.map +1 -0
- package/dist/src/control/index.js +4 -0
- package/dist/src/control/index.js.map +1 -0
- package/dist/src/control/protocol.d.ts +54 -0
- package/dist/src/control/protocol.d.ts.map +1 -0
- package/dist/src/control/protocol.js +60 -0
- package/dist/src/control/protocol.js.map +1 -0
- package/dist/src/control/server.d.ts +52 -0
- package/dist/src/control/server.d.ts.map +1 -0
- package/dist/src/control/server.js +199 -0
- package/dist/src/control/server.js.map +1 -0
- package/dist/src/daemon/handles.d.ts +35 -6
- package/dist/src/daemon/handles.d.ts.map +1 -1
- package/dist/src/daemon/handles.js +83 -28
- package/dist/src/daemon/handles.js.map +1 -1
- package/dist/src/daemon/index.d.ts +2 -3
- package/dist/src/daemon/index.d.ts.map +1 -1
- package/dist/src/daemon/index.js +2 -3
- package/dist/src/daemon/index.js.map +1 -1
- package/dist/src/daemon/methods.d.ts +13 -0
- package/dist/src/daemon/methods.d.ts.map +1 -1
- package/dist/src/daemon/methods.js +50 -1
- package/dist/src/daemon/methods.js.map +1 -1
- package/dist/src/hooks/command-scanner.d.ts +5 -0
- package/dist/src/hooks/command-scanner.d.ts.map +1 -0
- package/dist/src/hooks/command-scanner.js +117 -0
- package/dist/src/hooks/command-scanner.js.map +1 -0
- package/dist/src/hooks/glob.d.ts +8 -0
- package/dist/src/hooks/glob.d.ts.map +1 -0
- package/dist/src/hooks/glob.js +98 -0
- package/dist/src/hooks/glob.js.map +1 -0
- package/dist/src/hooks/index.d.ts +9 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js +9 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/hooks/install.d.ts +29 -0
- package/dist/src/hooks/install.d.ts.map +1 -0
- package/dist/src/hooks/install.js +86 -0
- package/dist/src/hooks/install.js.map +1 -0
- package/dist/src/hooks/path-blocker.d.ts +29 -0
- package/dist/src/hooks/path-blocker.d.ts.map +1 -0
- package/dist/src/hooks/path-blocker.js +59 -0
- package/dist/src/hooks/path-blocker.js.map +1 -0
- package/dist/src/hooks/post-tool-use.d.ts +13 -0
- package/dist/src/hooks/post-tool-use.d.ts.map +1 -0
- package/dist/src/hooks/post-tool-use.js +45 -0
- package/dist/src/hooks/post-tool-use.js.map +1 -0
- package/dist/src/hooks/pre-tool-use.d.ts +8 -0
- package/dist/src/hooks/pre-tool-use.d.ts.map +1 -0
- package/dist/src/hooks/pre-tool-use.js +38 -0
- package/dist/src/hooks/pre-tool-use.js.map +1 -0
- package/dist/src/hooks/protocol.d.ts +41 -0
- package/dist/src/hooks/protocol.d.ts.map +1 -0
- package/dist/src/hooks/protocol.js +27 -0
- package/dist/src/hooks/protocol.js.map +1 -0
- package/dist/src/hooks/redactor.d.ts +19 -0
- package/dist/src/hooks/redactor.d.ts.map +1 -0
- package/dist/src/hooks/redactor.js +71 -0
- package/dist/src/hooks/redactor.js.map +1 -0
- package/dist/src/mcp/index.d.ts +4 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +4 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/protocol.d.ts +98 -0
- package/dist/src/mcp/protocol.d.ts.map +1 -0
- package/dist/src/mcp/protocol.js +79 -0
- package/dist/src/mcp/protocol.js.map +1 -0
- package/dist/src/mcp/server.d.ts +46 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +108 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools.d.ts +16 -0
- package/dist/src/mcp/tools.d.ts.map +1 -0
- package/dist/src/mcp/tools.js +117 -0
- package/dist/src/mcp/tools.js.map +1 -0
- package/dist/src/policy/evaluate.d.ts +13 -0
- package/dist/src/policy/evaluate.d.ts.map +1 -0
- package/dist/src/policy/evaluate.js +73 -0
- package/dist/src/policy/evaluate.js.map +1 -0
- package/dist/src/policy/index.d.ts +5 -0
- package/dist/src/policy/index.d.ts.map +1 -0
- package/dist/src/policy/index.js +5 -0
- package/dist/src/policy/index.js.map +1 -0
- package/dist/src/policy/loader.d.ts +33 -0
- package/dist/src/policy/loader.d.ts.map +1 -0
- package/dist/src/policy/loader.js +170 -0
- package/dist/src/policy/loader.js.map +1 -0
- package/dist/src/policy/template.d.ts +10 -0
- package/dist/src/policy/template.d.ts.map +1 -0
- package/dist/src/policy/template.js +69 -0
- package/dist/src/policy/template.js.map +1 -0
- package/dist/src/policy/types.d.ts +62 -0
- package/dist/src/policy/types.d.ts.map +1 -0
- package/dist/src/policy/types.js +10 -0
- package/dist/src/policy/types.js.map +1 -0
- package/package.json +9 -3
- package/dist/src/bin/sigild.d.ts +0 -3
- package/dist/src/bin/sigild.d.ts.map +0 -1
- package/dist/src/bin/sigild.js +0 -30
- package/dist/src/bin/sigild.js.map +0 -1
- package/dist/src/daemon/rpc.d.ts +0 -61
- package/dist/src/daemon/rpc.d.ts.map +0 -1
- package/dist/src/daemon/rpc.js +0 -76
- package/dist/src/daemon/rpc.js.map +0 -1
- package/dist/src/daemon/runtime.d.ts +0 -40
- package/dist/src/daemon/runtime.d.ts.map +0 -1
- package/dist/src/daemon/runtime.js +0 -61
- package/dist/src/daemon/runtime.js.map +0 -1
- package/dist/src/daemon/server.d.ts +0 -53
- package/dist/src/daemon/server.d.ts.map +0 -1
- package/dist/src/daemon/server.js +0 -103
- package/dist/src/daemon/server.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,69 +2,178 @@
|
|
|
2
2
|
|
|
3
3
|
> Claude can sign, but never see.
|
|
4
4
|
|
|
5
|
-
`sigil` is a local signing
|
|
5
|
+
`sigil` is a local signing tool and Claude Code integration that lets agentic coding tools use private keys without ever putting key material in the model's context window.
|
|
6
6
|
|
|
7
|
-
**Status:** pre-alpha.
|
|
7
|
+
**Status:** pre-alpha. The MCP server, CLI, unlock flow, ward hooks, and policy engine (static checks) all work end-to-end. Out-of-band confirmation, rolling-window value caps, and EIP-712 domain allowlists are not yet implemented. Until they land — and until the supply-chain attestations promised for v0.1.0 ship — **do not use this with real funds yet.** Build plan lives in the [tracking issue](https://github.com/cdrn/sigil/issues/9).
|
|
8
8
|
|
|
9
9
|
## What it is
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
One MCP server process, four bins, three runtime deps:
|
|
12
12
|
|
|
13
|
-
1. **`
|
|
14
|
-
2. **`sigil
|
|
15
|
-
3.
|
|
13
|
+
1. **`sigil-mcp`** — the only thing that runs. Claude Code spawns it per session via your `mcpServers` config; it dies when Claude exits. Holds unlocked keys in process memory (zeroized on shutdown, `sigil lock`, or unlock-failure; mlock against swap is planned). Keys at rest are encrypted with XChaCha20-Poly1305 and an Argon2id-derived key. Signs over stdio using a DIY MCP wire protocol (~200 lines, no SDK dep). Claude never sees key material — only opaque handles like `eth:executor`.
|
|
14
|
+
2. **`sigil`** — control CLI. `init`, `status`, `portal add`/`list`/`remove`, `unlock`, `lock`.
|
|
15
|
+
3. **`sigil-hook-pre` / `sigil-hook-post`** — Claude Code hook binaries that block reads of common key paths and redact key-shaped strings from tool output.
|
|
16
|
+
|
|
17
|
+
`sigil-mcp` boots **locked**: empty in-memory handle table, no keys loaded. Sign methods return `DAEMON_LOCKED` (-32003) with a "run sigil unlock" message until you push the passphrase in from a separate terminal via `sigil unlock`. That CLI connects to a Unix socket at `~/.sigil/control.sock` (0600) that `sigil-mcp` opens at startup. After unlock, signs work for the rest of the session; `sigil lock` zeroizes the table without killing the process.
|
|
18
|
+
|
|
19
|
+
Sign methods exposed today: EIP-191 personal_sign, EIP-1559 + legacy transactions, EIP-712 typed data.
|
|
16
20
|
|
|
17
21
|
## What it isn't
|
|
18
22
|
|
|
19
23
|
- Not a hardware wallet replacement. If you can use a Ledger or YubiKey, do that.
|
|
20
24
|
- Not a custody solution. It runs on your laptop or VPS and protects you from one specific class of failure: leaking key material through an LLM agent.
|
|
21
|
-
-
|
|
25
|
+
- A first cut of *bounding signing authority* via the policy engine — but not the full thing. v1 covers static checks (chain ID, destination allowlist, per-tx value cap, function-selector allowlist, on/off toggles for personal_sign and EIP-712). Rolling-window caps, EIP-712 domain allowlists, and out-of-band human confirmation are tracked in [#3](https://github.com/cdrn/sigil/issues/3) + [#4](https://github.com/cdrn/sigil/issues/4) and will land incrementally.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install -g sigild
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This drops four binaries on your `$PATH`: `sigil`, `sigil-mcp`, `sigil-hook-pre`, `sigil-hook-post`. (The package name on npm is `sigild` for legacy reasons; the bins do not include a daemon any more.)
|
|
34
|
+
|
|
35
|
+
Requires Node 22+.
|
|
22
36
|
|
|
23
|
-
##
|
|
37
|
+
## Quick start
|
|
24
38
|
|
|
25
|
-
```
|
|
26
|
-
#
|
|
27
|
-
sigil init
|
|
28
|
-
sigil up # starts the daemon, unlocks keys via OS keychain
|
|
39
|
+
```sh
|
|
40
|
+
# 1. Wire sigil into Claude Code (project-scoped). Pass --user to do it globally.
|
|
41
|
+
sigil init
|
|
29
42
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
# 2. Encrypt a private key into sigil's keystore. Source key is deleted by default.
|
|
44
|
+
# Accepts either 32 raw bytes or 64 hex chars (with optional 0x prefix).
|
|
45
|
+
sigil portal add eth:bot --key-file ./bot.key
|
|
46
|
+
# → prompts for a passphrase, derives the address, writes
|
|
47
|
+
# ~/.sigil/keys/eth:bot.sigil AND ~/.sigil/policy/eth:bot.toml (permissive)
|
|
48
|
+
#
|
|
49
|
+
# Pass --strict to start with a locked-down policy template you fill in
|
|
50
|
+
# before any sign succeeds:
|
|
51
|
+
# sigil portal add eth:bot --key-file ./bot.key --strict
|
|
35
52
|
|
|
36
|
-
# Claude
|
|
37
|
-
#
|
|
53
|
+
# 3. Open Claude Code. It spawns sigil-mcp automatically via your MCP config.
|
|
54
|
+
# sigil-mcp boots locked — the first sign attempt will return DAEMON_LOCKED.
|
|
55
|
+
|
|
56
|
+
# 4. In a separate terminal, push the passphrase to the running sigil-mcp.
|
|
57
|
+
sigil unlock
|
|
58
|
+
# → prompts for the passphrase, decrypts every keyfile in ~/.sigil/keys/
|
|
59
|
+
|
|
60
|
+
# 5. Use Claude Code. The four sigil_* tools will work for the rest of the session.
|
|
61
|
+
|
|
62
|
+
# Optional: re-lock without restarting Claude.
|
|
63
|
+
sigil lock
|
|
38
64
|
```
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
If you close Claude Code, `sigil-mcp` exits and its memory is wiped. Open a new session and `sigil unlock` again — the encrypted keyfiles on disk persist.
|
|
67
|
+
|
|
68
|
+
## CLI reference
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
sigil init [--user]
|
|
72
|
+
Write the MCP server registration and the ward hooks into
|
|
73
|
+
.claude/settings.json. With --user, writes ~/.claude/settings.json
|
|
74
|
+
instead. Idempotent — preserves your unrelated settings.
|
|
75
|
+
|
|
76
|
+
sigil portal add <handle> --key-file <path> [--no-remove-source] [--strict]
|
|
77
|
+
Encrypt the key with your passphrase and store it at
|
|
78
|
+
~/.sigil/keys/<handle>.sigil (mode 0600). Handle format is
|
|
79
|
+
<kind>:<name> where kind is "eth". The source key file is deleted
|
|
80
|
+
by default — pass --no-remove-source to keep it.
|
|
81
|
+
Also writes ~/.sigil/policy/<handle>.toml — permissive by default
|
|
82
|
+
(signs anything), or use --strict for a locked-down template you
|
|
83
|
+
fill in before signs succeed.
|
|
84
|
+
|
|
85
|
+
sigil policy show <handle>
|
|
86
|
+
Print the current policy file for a portal. Validates schema; exits
|
|
87
|
+
1 if the file is missing or malformed.
|
|
88
|
+
|
|
89
|
+
sigil portal list
|
|
90
|
+
List the encrypted keyfiles on disk with their derived addresses.
|
|
91
|
+
Requires the passphrase.
|
|
92
|
+
|
|
93
|
+
sigil portal remove <handle>
|
|
94
|
+
Delete a keyfile from disk.
|
|
95
|
+
|
|
96
|
+
sigil unlock
|
|
97
|
+
Prompt for the passphrase and push it to the running sigil-mcp over
|
|
98
|
+
the control socket. After unlock, sign calls succeed for the rest
|
|
99
|
+
of the Claude session. Fails if sigil-mcp is not running (start a
|
|
100
|
+
Claude Code session first) or if already unlocked.
|
|
101
|
+
|
|
102
|
+
sigil lock
|
|
103
|
+
Tell sigil-mcp to zeroize and clear its in-memory keys. Re-unlock
|
|
104
|
+
with sigil unlock — sigil-mcp keeps running.
|
|
105
|
+
|
|
106
|
+
sigil status
|
|
107
|
+
Report whether sigil-mcp is running (probes ~/.sigil/control.sock),
|
|
108
|
+
its PID, whether it's unlocked, what portals it has loaded, and
|
|
109
|
+
how many keyfiles exist on disk. Does not require the passphrase.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Set `SIGIL_HOME` to override `~/.sigil`. Set `SIGIL_CONTROL_SOCK` to override the control socket path.
|
|
113
|
+
|
|
114
|
+
## Multi-window behaviour
|
|
115
|
+
|
|
116
|
+
Each Claude Code window spawns its own `sigil-mcp`. They share the on-disk keyfiles + audit log but have separate in-memory handle tables — you `sigil unlock` once per window. (The first MCP to start owns `control.sock`; further sessions will get their own socket once flock-based per-instance sockets land in Phase C of [#23](https://github.com/cdrn/sigil/issues/23). Until then, only the first window's `sigil-mcp` is reachable from the CLI.)
|
|
117
|
+
|
|
118
|
+
OS-keychain integration (planned, v0.3) will make unlock zero-touch for users who set it up.
|
|
119
|
+
|
|
120
|
+
## Policy engine
|
|
121
|
+
|
|
122
|
+
Once a portal is unlocked, signing authority over its key is real. To bound the blast radius of a successful prompt injection, every portal has a policy file at `~/.sigil/policy/<handle>.toml`. Two modes:
|
|
123
|
+
|
|
124
|
+
**Permissive** (default for `sigil portal add`): no rules. Sign anything the agent asks. The key isolation guarantees still hold — your key never enters the agent's context — but the unlocked portal can be made to sign whatever an attacker can get the agent to ask for. Useful for: testnet bots, demo flows, anyone who only cares about the context-window protection.
|
|
125
|
+
|
|
126
|
+
**Strict** (opt in with `--strict`): every sign request is checked. Generated template:
|
|
41
127
|
|
|
42
128
|
```toml
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
allowed_selectors = [
|
|
49
|
-
|
|
129
|
+
mode = "strict"
|
|
130
|
+
|
|
131
|
+
chain_ids = [1] # allowed chain IDs
|
|
132
|
+
allow_to = [] # allowed destination addresses (lowercase 0x)
|
|
133
|
+
max_value_wei = "0" # per-tx cap, in wei, as decimal string
|
|
134
|
+
allowed_selectors = [] # 4-byte function selectors, e.g. "0xa9059cbb"
|
|
135
|
+
|
|
136
|
+
allow_message_signing = false # EIP-191 personal_sign (e.g. SIWE)
|
|
137
|
+
allow_typed_data = false # EIP-712 (Permit, OpenSea — can be financial)
|
|
50
138
|
```
|
|
51
139
|
|
|
140
|
+
A failed rule throws `POLICY_DENIED` (-32001) back to the agent with the human-readable reason ("tx denied — value X exceeds max_value_wei Y"), and the deny is appended to the hash-chained audit log alongside allows. Denies are forensically the more interesting half — they're the prompt-injection canary.
|
|
141
|
+
|
|
142
|
+
What's deferred to follow-up PRs (still in [#3](https://github.com/cdrn/sigil/issues/3)): rolling-window value caps (e.g. 1 ETH/day per portal), EIP-712 domain + primary-type allowlists, decoded-calldata arg checks, and the `require_confirm_above_wei` outcome that hooks into the OOB push gate ([#4](https://github.com/cdrn/sigil/issues/4)).
|
|
143
|
+
|
|
52
144
|
## Supply chain posture
|
|
53
145
|
|
|
54
146
|
Key-management libraries die from supply chain compromise, not from clever attacks on the code. Given the npm ecosystem in 2026 (Mini Shai-Hulud, Axios, pgserve, TanStack), `sigil` commits to:
|
|
55
147
|
|
|
56
|
-
- **Zero install scripts.** No `postinstall`, `preinstall`, `prepare`. CI-enforced.
|
|
57
|
-
- **
|
|
148
|
+
- **Zero install scripts.** No `postinstall`, `preinstall`, `prepare`. CI-enforced (planned: a CI guard that fails if any dep adds one).
|
|
149
|
+
- **Three runtime deps, all version-pinned** (no caret ranges):
|
|
150
|
+
- [`@noble/ciphers`](https://github.com/paulmillr/noble-ciphers) for XChaCha20-Poly1305
|
|
151
|
+
- [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) for Argon2id, keccak256, sha2, HMAC
|
|
152
|
+
- [`@noble/secp256k1`](https://github.com/paulmillr/noble-secp256k1) for ECDSA
|
|
153
|
+
All by paulmillr, audited, zero transitive deps.
|
|
154
|
+
- **No MCP SDK.** The official `@modelcontextprotocol/sdk` pulls 92 transitive deps (ajv, hono, cors, cross-spawn, etc) — unacceptable surface. We implement the MCP wire protocol directly in ~200 lines.
|
|
58
155
|
- **No Bun.** Plain Node only. Bun is currently being weaponized by Mini Shai-Hulud as an evasion layer; we will not give that pattern any cover.
|
|
59
|
-
- **
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
156
|
+
- **Planned for v0.1.0** (the real release, not this 0.0.1 placeholder):
|
|
157
|
+
- Provenance attestations on every npm publish via GitHub Actions trusted publishing (OIDC, no long-lived tokens)
|
|
158
|
+
- SBOM (CycloneDX) attached to every release
|
|
159
|
+
- Signed standalone binaries from GitHub Releases for users who'd rather not touch npm
|
|
160
|
+
- Action SHA pinning rotation via Dependabot
|
|
63
161
|
|
|
64
162
|
## Threat model
|
|
65
163
|
|
|
66
164
|
See [THREAT_MODEL.md](./THREAT_MODEL.md). Read it before trusting this with anything.
|
|
67
165
|
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
git clone https://github.com/cdrn/sigil
|
|
170
|
+
cd sigil
|
|
171
|
+
npm install # respects .npmrc ignore-scripts=true
|
|
172
|
+
npm test # builds + runs ~330 tests; should finish in under 10s
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the PR-per-layer workflow.
|
|
176
|
+
|
|
68
177
|
## License
|
|
69
178
|
|
|
70
179
|
Apache License 2.0. See [LICENSE](./LICENSE).
|
package/THREAT_MODEL.md
CHANGED
|
@@ -13,10 +13,24 @@ This document describes what `sigil` defends against, what it doesn't, and the a
|
|
|
13
13
|
|
|
14
14
|
In order of value:
|
|
15
15
|
|
|
16
|
-
1. **Private key material.** Must never leave `
|
|
17
|
-
2. **Signing authority.** A signature `
|
|
16
|
+
1. **Private key material.** Must never leave `sigil-mcp`'s address space.
|
|
17
|
+
2. **Signing authority.** A signature `sigil-mcp` produces on behalf of a portal is a financial action. The set of authorized actions is bounded by per-portal policy.
|
|
18
18
|
3. **The audit log.** Append-only record of every sign decision. Tampering with it defeats post-incident forensics.
|
|
19
19
|
|
|
20
|
+
## Runtime model
|
|
21
|
+
|
|
22
|
+
There is exactly one long-running sigil process: `sigil-mcp`, spawned by Claude Code per session via your `mcpServers` config. It dies when Claude exits.
|
|
23
|
+
|
|
24
|
+
Keys at rest are encrypted on disk in `~/.sigil/keys/<handle>.sigil` (XChaCha20-Poly1305, Argon2id-derived key). They are **not** loaded into `sigil-mcp`'s memory at startup. `sigil-mcp` boots with an empty in-memory handle table; sign requests return `DAEMON_LOCKED` until the user explicitly pushes the passphrase in.
|
|
25
|
+
|
|
26
|
+
The unlock path:
|
|
27
|
+
|
|
28
|
+
1. `sigil-mcp` opens a Unix socket at `~/.sigil/control.sock` (chmod 0600).
|
|
29
|
+
2. The user runs `sigil unlock` in a separate terminal; the CLI prompts for the passphrase, connects to the socket, and sends `{method: "unlock", passphraseB64: ...}`.
|
|
30
|
+
3. `sigil-mcp` decrypts every keyfile in `~/.sigil/keys/`, populates the in-memory table, zeroizes the passphrase buffer.
|
|
31
|
+
|
|
32
|
+
This shape was chosen so the agent has no path to trigger unlock — only a human at the local TTY can. `sigil lock` zeroizes the in-memory table without killing the process; subsequent signs return `DAEMON_LOCKED` again until the next `sigil unlock`.
|
|
33
|
+
|
|
20
34
|
## In scope
|
|
21
35
|
|
|
22
36
|
### 1. Preventing key ingestion by the agent
|
|
@@ -24,8 +38,9 @@ In order of value:
|
|
|
24
38
|
The primary threat. Failure mode: a private key ends up in the agent's context window, then in transcripts, prompt caches, cloud logs, or attacker-controlled exfiltration paths.
|
|
25
39
|
|
|
26
40
|
Defenses:
|
|
27
|
-
- Keys live in `
|
|
41
|
+
- Keys live in `sigil-mcp`'s memory only, unlocked from encrypted at-rest storage. Plaintext is zeroized on shutdown, on `sigil lock`, and on any failed unlock. mlock against swap is planned (requires a native module distributed as bundled prebuilds, see Known limitations).
|
|
28
42
|
- The MCP interface exposes only opaque handles (`eth:executor`), never key bytes.
|
|
43
|
+
- The unlock passphrase is delivered out-of-band via the control socket, not through the agent. The agent has no exposed surface that can trigger an unlock.
|
|
29
44
|
- PreToolUse hooks block `Read` and `Bash` access to `~/.sigil/**`, `*.pem`, `**/.env*`, `**/keystore*`, and a configurable extra list.
|
|
30
45
|
- PostToolUse output filter redacts hex-encoded 32-byte blobs, PEM blocks, and bip39-shaped strings from tool output before it reaches the model.
|
|
31
46
|
|
|
@@ -33,13 +48,24 @@ Defenses:
|
|
|
33
48
|
|
|
34
49
|
A defended key still loses you money if the agent can be tricked into signing the wrong payload. Prompt injection through transaction calldata, web content, or repo files can redirect signing intent.
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
51
|
+
The policy engine (per-portal `~/.sigil/policy/<handle>.toml`) is the layer that bounds this. v1 ships static checks; further work is staged in [#3](https://github.com/cdrn/sigil/issues/3) and [#4](https://github.com/cdrn/sigil/issues/4).
|
|
52
|
+
|
|
53
|
+
**Shipped (v1):**
|
|
54
|
+
- Two-mode policy. `permissive` mode (default for `sigil portal add`) does no checks — useful for users who only want the key-isolation guarantee. `strict` mode enforces every rule below.
|
|
55
|
+
- Destination allowlist (`allow_to`).
|
|
56
|
+
- Chain ID allowlist (`chain_ids`).
|
|
57
|
+
- Per-tx value cap (`max_value_wei`).
|
|
58
|
+
- Function selector allowlist (`allowed_selectors`) — 4-byte selector match, no calldata decoding.
|
|
59
|
+
- On/off toggles for personal_sign (`allow_message_signing`) and EIP-712 typed data (`allow_typed_data`).
|
|
60
|
+
- Append-only hash-chained audit log records both allow AND deny decisions. Denies are the prompt-injection canary.
|
|
61
|
+
- Runtime fail-closed when the policy file is missing for a portal — keys can't be used without an explicit decision.
|
|
62
|
+
|
|
63
|
+
**Planned:**
|
|
64
|
+
- Rolling-window value caps (`max_value_per_hour_wei`, etc.) backed by a per-portal ledger with flock for multi-instance safety.
|
|
65
|
+
- EIP-712 domain + primary-type allowlists (today `allow_typed_data` is binary; OpenSea-style approvals deserve finer control).
|
|
66
|
+
- Decoded-calldata arg checks ("allow `transfer(addr, amt)` only when `amt <= 100e18` and `addr in list`"). Needs a BYO-ABI registry per portal.
|
|
67
|
+
- Out-of-band human confirmation above a configurable value threshold (push to ntfy / Pushover / Telegram / Apple Push), [#4](https://github.com/cdrn/sigil/issues/4).
|
|
68
|
+
- `allow_contract_creation` toggle — currently strict mode always denies tx with `to: null`.
|
|
43
69
|
|
|
44
70
|
### 3. Supply chain compromise of `sigil` itself
|
|
45
71
|
|
|
@@ -47,16 +73,16 @@ Given the 2026 npm threat landscape, a compromised release of `sigil` would be c
|
|
|
47
73
|
|
|
48
74
|
- Zero install scripts (`postinstall`, `preinstall`, `prepare`). CI-enforced.
|
|
49
75
|
- Minimal, audited dependencies (`@noble/*` family, plain Node stdlib).
|
|
50
|
-
- Provenance attestations via GitHub Actions trusted publishing (OIDC, no long-lived tokens).
|
|
51
|
-
- Signed standalone binaries as an alternative distribution channel.
|
|
52
|
-
- Reproducible builds, published SBOM, public release checksums.
|
|
76
|
+
- Provenance attestations via GitHub Actions trusted publishing (OIDC, no long-lived tokens) — planned for v0.1.0, not yet in place.
|
|
77
|
+
- Signed standalone binaries as an alternative distribution channel — planned for v0.1.0.
|
|
78
|
+
- Reproducible builds, published SBOM, public release checksums — planned for v0.1.0.
|
|
53
79
|
- No Bun in any distributed artifact.
|
|
54
80
|
|
|
55
81
|
## Out of scope
|
|
56
82
|
|
|
57
83
|
`sigil` does not defend against:
|
|
58
84
|
|
|
59
|
-
- **Local code execution as your user.** If an attacker can run code as `$USER`, they can `ptrace`
|
|
85
|
+
- **Local code execution as your user.** If an attacker can run code as `$USER`, they can `ptrace` `sigil-mcp`, read mlock'd memory, connect to the control socket and unlock, or simply call the MCP tools. `sigil` makes this harder (audit log will show it, policy will apply once #3 lands) but does not prevent it.
|
|
60
86
|
- **Root or kernel compromise.** mlock prevents swap; it does not prevent a root user from reading process memory.
|
|
61
87
|
- **Side channels on shared hardware.** Cache timing, Rowhammer, etc.
|
|
62
88
|
- **Physical access to an unlocked machine.**
|
|
@@ -67,17 +93,19 @@ Given the 2026 npm threat landscape, a compromised release of `sigil` would be c
|
|
|
67
93
|
## Assumptions
|
|
68
94
|
|
|
69
95
|
- The user installs `sigil` through a trusted channel (signed release, verified npm provenance, or built from source).
|
|
70
|
-
- The host OS enforces standard process isolation.
|
|
96
|
+
- The host OS enforces standard process isolation and respects file permissions on `~/.sigil/` (the directory is 0700; keyfiles and the control socket are 0600).
|
|
71
97
|
- The user's OS keychain (or chosen unlock mechanism) is not compromised.
|
|
72
98
|
- Clock skew on the host is bounded (matters for rolling-window policy and audit timestamps).
|
|
73
99
|
|
|
74
100
|
## Known limitations
|
|
75
101
|
|
|
76
|
-
- The hook-based path blocker is best-effort: it covers `Read` and `Bash`, but a sufficiently creative agent could still ask another tool to do the read. The defense in depth is that even if a key file is read, its contents are redacted by the output filter before reaching the model.
|
|
102
|
+
- The hook-based path blocker is best-effort: it covers `Read` and `Bash`, but a sufficiently creative agent could still ask another tool to do the read. The defense in depth is that even if a key file is read, its contents are redacted by the output filter before reaching the model. And, given the unlock model, reading the encrypted keyfile alone yields nothing — the agent would also need the passphrase, which is never in its context.
|
|
77
103
|
- The output redaction filter has false negatives (keys with non-standard encoding) and false positives (legitimate hex blobs). It is not a replacement for the path blocker; it's a second line.
|
|
78
|
-
- The MCP socket is
|
|
79
|
-
- **mlock is not yet implemented.** Plaintext key material lives in a regular `Buffer` that is zeroized on
|
|
80
|
-
-
|
|
104
|
+
- The MCP stdio is the agent's exposed surface; the control socket is the user's exposed surface. Both are unauthenticated within the user's session — any process running as `$USER` can talk to either. This is consistent with the threat model (we don't defend against local user compromise) but worth stating explicitly.
|
|
105
|
+
- **mlock is not yet implemented.** Plaintext key material lives in a regular `Buffer` that is zeroized on `sigil-mcp` shutdown, on `sigil lock`, or on a failed unlock. This means keys are vulnerable to being paged to swap on a memory-pressured system. mlock requires a native module, which we will ship as bundled prebuilds (no install scripts) rather than via a compile-on-install dependency. Tracked as a planned layer.
|
|
106
|
+
- **Passphrase in transit through V8 strings.** The control socket carries the passphrase base64-encoded inside a JSON message. The CLI's `Buffer` and the server's decoded `Buffer` are zeroized after use, but the intermediate JSON string sits in V8's string heap and cannot be reliably wiped. This is the same trade-off as `readPassphrase`'s internal accumulator and is considered acceptable for v0.x; mitigations would require either a custom binary framing or a fully native crypto path.
|
|
107
|
+
- **Multi-window:** the first `sigil-mcp` to start owns `~/.sigil/control.sock`. Subsequent sessions in other Claude windows still run, but their `sigil-mcp` cannot be reached by `sigil unlock` — only the first one is addressable. Phase C of [#23](https://github.com/cdrn/sigil/issues/23) will add per-PID sockets + a flock'd audit log so multi-window sessions coexist cleanly.
|
|
108
|
+
- Out-of-band confirmation (planned, [#4](https://github.com/cdrn/sigil/issues/4)) depends on a working push channel. If the push provider is down, high-value signs are denied, not approved.
|
|
81
109
|
|
|
82
110
|
## Reporting issues
|
|
83
111
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil-hook-post.d.ts","sourceRoot":"","sources":["../../../src/bin/sigil-hook-post.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { decidePostToolUse } from '../hooks/post-tool-use.js';
|
|
3
|
+
import { readHookEnvelope } from '../hooks/protocol.js';
|
|
4
|
+
async function main() {
|
|
5
|
+
const env = await readHookEnvelope();
|
|
6
|
+
const modification = decidePostToolUse(env);
|
|
7
|
+
if (modification !== null) {
|
|
8
|
+
process.stdout.write(JSON.stringify(modification) + '\n');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
main().catch((err) => {
|
|
12
|
+
process.stderr.write(`sigil-hook-post: ${err.message}\n`);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
});
|
|
15
|
+
//# sourceMappingURL=sigil-hook-post.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil-hook-post.js","sourceRoot":"","sources":["../../../src/bin/sigil-hook-post.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACrC,MAAM,YAAY,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAC5C,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil-hook-pre.d.ts","sourceRoot":"","sources":["../../../src/bin/sigil-hook-pre.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { decidePreToolUse } from '../hooks/pre-tool-use.js';
|
|
3
|
+
import { readHookEnvelope } from '../hooks/protocol.js';
|
|
4
|
+
async function main() {
|
|
5
|
+
const env = await readHookEnvelope();
|
|
6
|
+
const decision = decidePreToolUse(env);
|
|
7
|
+
if (decision !== null) {
|
|
8
|
+
process.stdout.write(JSON.stringify(decision) + '\n');
|
|
9
|
+
}
|
|
10
|
+
// No decision = allow (exit 0 with empty stdout).
|
|
11
|
+
}
|
|
12
|
+
main().catch((err) => {
|
|
13
|
+
// On internal failure, default to allow so we don't deadlock the agent —
|
|
14
|
+
// but log loudly so the user notices something is wrong.
|
|
15
|
+
process.stderr.write(`sigil-hook-pre: ${err.message}\n`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=sigil-hook-pre.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil-hook-pre.js","sourceRoot":"","sources":["../../../src/bin/sigil-hook-pre.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;IACxD,CAAC;IACD,kDAAkD;AACpD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;IAC1B,yEAAyE;IACzE,yDAAyD;IACzD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil-mcp.d.ts","sourceRoot":"","sources":["../../../src/bin/sigil-mcp.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { AuditWriter } from '../audit/index.js';
|
|
4
|
+
import { resolvePaths } from '../cli/paths.js';
|
|
5
|
+
import { startControlServer } from '../control/index.js';
|
|
6
|
+
import { HandleTable } from '../daemon/handles.js';
|
|
7
|
+
import { runMcpStdio } from '../mcp/server.js';
|
|
8
|
+
import { FileSystemPolicyResolver } from '../policy/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* sigil-mcp: single-process MCP server for sigil. Spawned by Claude Code per
|
|
11
|
+
* session via the .claude/settings.json mcpServers entry.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
* - On startup: ensures ~/.sigil/{,keys/} exist (0o700), opens the audit
|
|
15
|
+
* log, constructs an EMPTY (locked) HandleTable, binds the control
|
|
16
|
+
* socket at ~/.sigil/control.sock (0o600).
|
|
17
|
+
* - During the session: runs the MCP wire loop on stdio. Sign methods
|
|
18
|
+
* return DAEMON_LOCKED until the user runs `sigil unlock`, which pushes
|
|
19
|
+
* the passphrase over the control socket; the HandleTable loads the
|
|
20
|
+
* encrypted keyfiles from disk and the session is unlocked.
|
|
21
|
+
* - On stdin close (Claude exited): closes the control socket, locks +
|
|
22
|
+
* disposes the HandleTable, closes the audit writer, exits.
|
|
23
|
+
*
|
|
24
|
+
* The control socket is unref'd so it doesn't keep the loop alive on its own;
|
|
25
|
+
* stdin alone gates process lifetime.
|
|
26
|
+
*/
|
|
27
|
+
async function main() {
|
|
28
|
+
const paths = resolvePaths(process.env);
|
|
29
|
+
mkdirSync(paths.home, { recursive: true, mode: 0o700 });
|
|
30
|
+
mkdirSync(paths.keysDir, { recursive: true, mode: 0o700 });
|
|
31
|
+
mkdirSync(paths.policyDir, { recursive: true, mode: 0o700 });
|
|
32
|
+
const handles = new HandleTable();
|
|
33
|
+
const audit = new AuditWriter(paths.auditLog);
|
|
34
|
+
const policy = new FileSystemPolicyResolver(paths.policyDir);
|
|
35
|
+
let controlClosed = false;
|
|
36
|
+
let control = null;
|
|
37
|
+
try {
|
|
38
|
+
control = await startControlServer({
|
|
39
|
+
socketPath: paths.controlSocket,
|
|
40
|
+
keysDir: paths.keysDir,
|
|
41
|
+
handles,
|
|
42
|
+
onLog: (e) => process.stderr.write(`control: ${JSON.stringify(e)}\n`),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// Another sigil-mcp already owns the control socket (or some other bind
|
|
47
|
+
// failure). Stay up without a control socket so the MCP stdio still
|
|
48
|
+
// works, but warn — this session will be permanently locked, since the
|
|
49
|
+
// primary sigil-mcp is what `sigil unlock` reaches.
|
|
50
|
+
process.stderr.write(`sigil-mcp: control socket unavailable (${err.message}). ` +
|
|
51
|
+
`This session will stay locked; sign calls will return DAEMON_LOCKED. ` +
|
|
52
|
+
`Run "sigil unlock" against the primary sigil-mcp from your first ` +
|
|
53
|
+
`Claude session — phase C of #23 will lift this limitation.\n`);
|
|
54
|
+
}
|
|
55
|
+
if (control) {
|
|
56
|
+
process.stderr.write(`sigil-mcp: ready (locked; run "sigil unlock" to load keys from ${paths.keysDir})\n`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
process.stderr.write(`sigil-mcp: ready (permanently locked — no control socket)\n`);
|
|
60
|
+
}
|
|
61
|
+
const shutdown = () => {
|
|
62
|
+
handles.dispose();
|
|
63
|
+
audit.close();
|
|
64
|
+
if (!controlClosed && control) {
|
|
65
|
+
controlClosed = true;
|
|
66
|
+
control.close().catch(() => { });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
process.on('exit', shutdown);
|
|
70
|
+
process.on('SIGINT', () => process.exit(0));
|
|
71
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
72
|
+
await runMcpStdio({
|
|
73
|
+
context: { handles, audit, policy },
|
|
74
|
+
stdin: process.stdin,
|
|
75
|
+
stdout: process.stdout,
|
|
76
|
+
onLog: (e) => process.stderr.write(JSON.stringify(e) + '\n'),
|
|
77
|
+
});
|
|
78
|
+
// stdin closed (Claude exited) — close the control socket explicitly so
|
|
79
|
+
// we don't leave a stale socket file behind.
|
|
80
|
+
if (control && !controlClosed) {
|
|
81
|
+
controlClosed = true;
|
|
82
|
+
await control.close();
|
|
83
|
+
}
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
process.stderr.write(`sigil-mcp: ${err.message}\n`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=sigil-mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil-mcp.js","sourceRoot":"","sources":["../../../src/bin/sigil-mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil.d.ts","sourceRoot":"","sources":["../../../src/bin/sigil.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from '../cli/main.js';
|
|
3
|
+
runCli({ argv: process.argv.slice(2) })
|
|
4
|
+
.then((exit) => process.exit(exit.code))
|
|
5
|
+
.catch((err) => {
|
|
6
|
+
process.stderr.write(`sigil: ${err.message}\n`);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
});
|
|
9
|
+
//# sourceMappingURL=sigil.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sigil.js","sourceRoot":"","sources":["../../../src/bin/sigil.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;KACvC,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;IACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;IAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny CLI arg-parsing wrapper around node:util.parseArgs.
|
|
3
|
+
* We support subcommands by consuming the first positional, then re-parsing
|
|
4
|
+
* the remainder with subcommand-specific options.
|
|
5
|
+
*/
|
|
6
|
+
export interface ParsedSubcommand {
|
|
7
|
+
command: string;
|
|
8
|
+
positionals: string[];
|
|
9
|
+
options: Record<string, string | boolean>;
|
|
10
|
+
}
|
|
11
|
+
export type OptionDef = {
|
|
12
|
+
type: 'string';
|
|
13
|
+
short?: string;
|
|
14
|
+
multiple?: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'boolean';
|
|
17
|
+
short?: string;
|
|
18
|
+
};
|
|
19
|
+
export interface SubcommandSpec {
|
|
20
|
+
options?: Record<string, OptionDef>;
|
|
21
|
+
}
|
|
22
|
+
export declare function parseSubcommand(argv: string[], specs: Record<string, SubcommandSpec>): ParsedSubcommand;
|
|
23
|
+
export declare class ArgsError extends Error {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../../src/cli/args.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;CAC3C;AAID,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACrC;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GACpC,gBAAgB,CA2BlB;AAED,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
export function parseSubcommand(argv, specs) {
|
|
3
|
+
if (argv.length === 0) {
|
|
4
|
+
throw new ArgsError('expected a subcommand');
|
|
5
|
+
}
|
|
6
|
+
const [head, ...rest] = argv;
|
|
7
|
+
if (head === undefined)
|
|
8
|
+
throw new ArgsError('expected a subcommand');
|
|
9
|
+
const spec = specs[head];
|
|
10
|
+
if (!spec) {
|
|
11
|
+
throw new ArgsError(`unknown subcommand "${head}"; expected one of: ${Object.keys(specs).join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = parseArgs({
|
|
16
|
+
args: rest,
|
|
17
|
+
allowPositionals: true,
|
|
18
|
+
options: spec.options ?? {},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
throw new ArgsError(`parsing "${head}": ${err.message}`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
command: head,
|
|
26
|
+
positionals: parsed.positionals,
|
|
27
|
+
options: parsed.values,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export class ArgsError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'ArgsError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/cli/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAwB,MAAM,WAAW,CAAC;AAwB5D,MAAM,UAAU,eAAe,CAC7B,IAAc,EACd,KAAqC;IAErC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAC/C,CAAC;IACD,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7B,IAAI,IAAI,KAAK,SAAS;QAAE,MAAM,IAAI,SAAS,CAAC,uBAAuB,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,SAAS,CACjB,uBAAuB,IAAI,uBAAuB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;YACjB,IAAI,EAAE,IAAI;YACV,gBAAgB,EAAE,IAAI;YACtB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;SACT,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,SAAS,CAAC,YAAY,IAAI,MAAO,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,OAAO;QACL,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,MAAM,CAAC,MAA0C;KAC3D,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,SAAU,SAAQ,KAAK;IAClC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { type SigilPaths, resolvePaths } from './paths.js';
|
|
2
|
+
export { ArgsError, parseSubcommand } from './args.js';
|
|
3
|
+
export { type PortalAddOpts, type PortalInfo, type PortalRemoveResult, portalAdd, portalListFromDisk, portalRemove, } from './portal.js';
|
|
4
|
+
export { type StatusReport, status } from './status.js';
|
|
5
|
+
export { type UnlockOpts, type LockOpts, type UnlockResult, formatResult, lock, unlock, } from './unlock.js';
|
|
6
|
+
export { type RunCliOpts, type CliExit, runCli } from './main.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,SAAS,EACT,kBAAkB,EAClB,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EACL,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,YAAY,EACZ,IAAI,EACJ,MAAM,GACP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC"}
|