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.
Files changed (163) hide show
  1. package/README.md +142 -33
  2. package/THREAT_MODEL.md +47 -19
  3. package/dist/src/bin/sigil-hook-post.d.ts +3 -0
  4. package/dist/src/bin/sigil-hook-post.d.ts.map +1 -0
  5. package/dist/src/bin/sigil-hook-post.js +15 -0
  6. package/dist/src/bin/sigil-hook-post.js.map +1 -0
  7. package/dist/src/bin/sigil-hook-pre.d.ts +3 -0
  8. package/dist/src/bin/sigil-hook-pre.d.ts.map +1 -0
  9. package/dist/src/bin/sigil-hook-pre.js +18 -0
  10. package/dist/src/bin/sigil-hook-pre.js.map +1 -0
  11. package/dist/src/bin/sigil-mcp.d.ts +3 -0
  12. package/dist/src/bin/sigil-mcp.d.ts.map +1 -0
  13. package/dist/src/bin/sigil-mcp.js +90 -0
  14. package/dist/src/bin/sigil-mcp.js.map +1 -0
  15. package/dist/src/bin/sigil.d.ts +3 -0
  16. package/dist/src/bin/sigil.d.ts.map +1 -0
  17. package/dist/src/bin/sigil.js +9 -0
  18. package/dist/src/bin/sigil.js.map +1 -0
  19. package/dist/src/cli/args.d.ts +26 -0
  20. package/dist/src/cli/args.d.ts.map +1 -0
  21. package/dist/src/cli/args.js +36 -0
  22. package/dist/src/cli/args.js.map +1 -0
  23. package/dist/src/cli/index.d.ts +7 -0
  24. package/dist/src/cli/index.d.ts.map +1 -0
  25. package/dist/src/cli/index.js +7 -0
  26. package/dist/src/cli/index.js.map +1 -0
  27. package/dist/src/cli/main.d.ts +26 -0
  28. package/dist/src/cli/main.d.ts.map +1 -0
  29. package/dist/src/cli/main.js +197 -0
  30. package/dist/src/cli/main.js.map +1 -0
  31. package/dist/src/cli/paths.d.ts +18 -0
  32. package/dist/src/cli/paths.d.ts.map +1 -0
  33. package/dist/src/cli/paths.js +13 -0
  34. package/dist/src/cli/paths.js.map +1 -0
  35. package/dist/src/cli/portal.d.ts +59 -0
  36. package/dist/src/cli/portal.d.ts.map +1 -0
  37. package/dist/src/cli/portal.js +112 -0
  38. package/dist/src/cli/portal.js.map +1 -0
  39. package/dist/src/cli/status.d.ts +28 -0
  40. package/dist/src/cli/status.d.ts.map +1 -0
  41. package/dist/src/cli/status.js +59 -0
  42. package/dist/src/cli/status.js.map +1 -0
  43. package/dist/src/cli/unlock.d.ts +36 -0
  44. package/dist/src/cli/unlock.d.ts.map +1 -0
  45. package/dist/src/cli/unlock.js +77 -0
  46. package/dist/src/cli/unlock.js.map +1 -0
  47. package/dist/src/control/client.d.ts +26 -0
  48. package/dist/src/control/client.d.ts.map +1 -0
  49. package/dist/src/control/client.js +76 -0
  50. package/dist/src/control/client.js.map +1 -0
  51. package/dist/src/control/index.d.ts +4 -0
  52. package/dist/src/control/index.d.ts.map +1 -0
  53. package/dist/src/control/index.js +4 -0
  54. package/dist/src/control/index.js.map +1 -0
  55. package/dist/src/control/protocol.d.ts +54 -0
  56. package/dist/src/control/protocol.d.ts.map +1 -0
  57. package/dist/src/control/protocol.js +60 -0
  58. package/dist/src/control/protocol.js.map +1 -0
  59. package/dist/src/control/server.d.ts +52 -0
  60. package/dist/src/control/server.d.ts.map +1 -0
  61. package/dist/src/control/server.js +199 -0
  62. package/dist/src/control/server.js.map +1 -0
  63. package/dist/src/daemon/handles.d.ts +35 -6
  64. package/dist/src/daemon/handles.d.ts.map +1 -1
  65. package/dist/src/daemon/handles.js +83 -28
  66. package/dist/src/daemon/handles.js.map +1 -1
  67. package/dist/src/daemon/index.d.ts +2 -3
  68. package/dist/src/daemon/index.d.ts.map +1 -1
  69. package/dist/src/daemon/index.js +2 -3
  70. package/dist/src/daemon/index.js.map +1 -1
  71. package/dist/src/daemon/methods.d.ts +13 -0
  72. package/dist/src/daemon/methods.d.ts.map +1 -1
  73. package/dist/src/daemon/methods.js +50 -1
  74. package/dist/src/daemon/methods.js.map +1 -1
  75. package/dist/src/hooks/command-scanner.d.ts +5 -0
  76. package/dist/src/hooks/command-scanner.d.ts.map +1 -0
  77. package/dist/src/hooks/command-scanner.js +117 -0
  78. package/dist/src/hooks/command-scanner.js.map +1 -0
  79. package/dist/src/hooks/glob.d.ts +8 -0
  80. package/dist/src/hooks/glob.d.ts.map +1 -0
  81. package/dist/src/hooks/glob.js +98 -0
  82. package/dist/src/hooks/glob.js.map +1 -0
  83. package/dist/src/hooks/index.d.ts +9 -0
  84. package/dist/src/hooks/index.d.ts.map +1 -0
  85. package/dist/src/hooks/index.js +9 -0
  86. package/dist/src/hooks/index.js.map +1 -0
  87. package/dist/src/hooks/install.d.ts +29 -0
  88. package/dist/src/hooks/install.d.ts.map +1 -0
  89. package/dist/src/hooks/install.js +86 -0
  90. package/dist/src/hooks/install.js.map +1 -0
  91. package/dist/src/hooks/path-blocker.d.ts +29 -0
  92. package/dist/src/hooks/path-blocker.d.ts.map +1 -0
  93. package/dist/src/hooks/path-blocker.js +59 -0
  94. package/dist/src/hooks/path-blocker.js.map +1 -0
  95. package/dist/src/hooks/post-tool-use.d.ts +13 -0
  96. package/dist/src/hooks/post-tool-use.d.ts.map +1 -0
  97. package/dist/src/hooks/post-tool-use.js +45 -0
  98. package/dist/src/hooks/post-tool-use.js.map +1 -0
  99. package/dist/src/hooks/pre-tool-use.d.ts +8 -0
  100. package/dist/src/hooks/pre-tool-use.d.ts.map +1 -0
  101. package/dist/src/hooks/pre-tool-use.js +38 -0
  102. package/dist/src/hooks/pre-tool-use.js.map +1 -0
  103. package/dist/src/hooks/protocol.d.ts +41 -0
  104. package/dist/src/hooks/protocol.d.ts.map +1 -0
  105. package/dist/src/hooks/protocol.js +27 -0
  106. package/dist/src/hooks/protocol.js.map +1 -0
  107. package/dist/src/hooks/redactor.d.ts +19 -0
  108. package/dist/src/hooks/redactor.d.ts.map +1 -0
  109. package/dist/src/hooks/redactor.js +71 -0
  110. package/dist/src/hooks/redactor.js.map +1 -0
  111. package/dist/src/mcp/index.d.ts +4 -0
  112. package/dist/src/mcp/index.d.ts.map +1 -0
  113. package/dist/src/mcp/index.js +4 -0
  114. package/dist/src/mcp/index.js.map +1 -0
  115. package/dist/src/mcp/protocol.d.ts +98 -0
  116. package/dist/src/mcp/protocol.d.ts.map +1 -0
  117. package/dist/src/mcp/protocol.js +79 -0
  118. package/dist/src/mcp/protocol.js.map +1 -0
  119. package/dist/src/mcp/server.d.ts +46 -0
  120. package/dist/src/mcp/server.d.ts.map +1 -0
  121. package/dist/src/mcp/server.js +108 -0
  122. package/dist/src/mcp/server.js.map +1 -0
  123. package/dist/src/mcp/tools.d.ts +16 -0
  124. package/dist/src/mcp/tools.d.ts.map +1 -0
  125. package/dist/src/mcp/tools.js +117 -0
  126. package/dist/src/mcp/tools.js.map +1 -0
  127. package/dist/src/policy/evaluate.d.ts +13 -0
  128. package/dist/src/policy/evaluate.d.ts.map +1 -0
  129. package/dist/src/policy/evaluate.js +73 -0
  130. package/dist/src/policy/evaluate.js.map +1 -0
  131. package/dist/src/policy/index.d.ts +5 -0
  132. package/dist/src/policy/index.d.ts.map +1 -0
  133. package/dist/src/policy/index.js +5 -0
  134. package/dist/src/policy/index.js.map +1 -0
  135. package/dist/src/policy/loader.d.ts +33 -0
  136. package/dist/src/policy/loader.d.ts.map +1 -0
  137. package/dist/src/policy/loader.js +170 -0
  138. package/dist/src/policy/loader.js.map +1 -0
  139. package/dist/src/policy/template.d.ts +10 -0
  140. package/dist/src/policy/template.d.ts.map +1 -0
  141. package/dist/src/policy/template.js +69 -0
  142. package/dist/src/policy/template.js.map +1 -0
  143. package/dist/src/policy/types.d.ts +62 -0
  144. package/dist/src/policy/types.d.ts.map +1 -0
  145. package/dist/src/policy/types.js +10 -0
  146. package/dist/src/policy/types.js.map +1 -0
  147. package/package.json +9 -3
  148. package/dist/src/bin/sigild.d.ts +0 -3
  149. package/dist/src/bin/sigild.d.ts.map +0 -1
  150. package/dist/src/bin/sigild.js +0 -30
  151. package/dist/src/bin/sigild.js.map +0 -1
  152. package/dist/src/daemon/rpc.d.ts +0 -61
  153. package/dist/src/daemon/rpc.d.ts.map +0 -1
  154. package/dist/src/daemon/rpc.js +0 -76
  155. package/dist/src/daemon/rpc.js.map +0 -1
  156. package/dist/src/daemon/runtime.d.ts +0 -40
  157. package/dist/src/daemon/runtime.d.ts.map +0 -1
  158. package/dist/src/daemon/runtime.js +0 -61
  159. package/dist/src/daemon/runtime.js.map +0 -1
  160. package/dist/src/daemon/server.d.ts +0 -53
  161. package/dist/src/daemon/server.d.ts.map +0 -1
  162. package/dist/src/daemon/server.js +0 -103
  163. 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 daemon and Claude Code integration that lets agentic coding tools use private keys without ever putting key material in the model's context window.
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. Threat model is committed before the code. Do not use this with real funds yet. Build plan and current progress live in the [tracking issue](https://github.com/cdrn/sigil/issues/9).
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
- Three components that work together:
11
+ One MCP server process, four bins, three runtime deps:
12
12
 
13
- 1. **`sigild`** — a long-running local daemon that holds unlocked keys in process memory (zeroized on shutdown; mlock against swap is planned, see [THREAT_MODEL.md](./THREAT_MODEL.md)) and exposes signing operations over a Unix socket. Keys at rest are encrypted with XChaCha20-Poly1305 and an Argon2id-derived key.
14
- 2. **`sigil-mcp`** — an MCP server that Claude (or any agentic tool) talks to. It forwards signing requests to `sigild` and returns signatures. Claude never sees key material — only opaque handles like `eth:executor`.
15
- 3. **Hooks (the wards)** — `sigil init` installs PreToolUse and PostToolUse hooks into `.claude/settings.json` that block `Read` and `Bash` from touching key paths, and redact key-shaped strings from tool output.
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
- - Not a substitute for policy. The library prevents *ingestion* of keys; the policy engine prevents *misuse* of signing authority. Both matter see the threat model.
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
- ## How it will work (sketch)
37
+ ## Quick start
24
38
 
25
- ```bash
26
- # one-time setup
27
- sigil init # installs hooks into ~/.claude/settings.json
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
- # register a key with policy
31
- sigil portal add executor \
32
- --type eth \
33
- --key-file ~/keys/bot.key \
34
- --policy ./policies/executor.toml
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 now sees `eth:executor` as a portal it can sign with,
37
- # but cannot read the key file or anything under ~/.sigil
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
- A policy file looks like:
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
- [portal.executor]
44
- chain_ids = [1, 42161, 8453]
45
- allow_to = ["0xRouter...", "0xExecutor..."]
46
- max_value_wei = "100000000000000000" # 0.1 ETH per tx
47
- max_value_per_hour_wei = "500000000000000000"
48
- allowed_selectors = ["0xa9059cbb", "0x095ea7b3"] # transfer, approve
49
- require_confirm_above_wei = "10000000000000000" # push to phone for human ack
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
- - **Minimal dependencies.** Core sign path uses [`@noble/secp256k1`](https://github.com/paulmillr/noble-secp256k1) and `@noble/hashes` (audited, zero-dep). No viem in the core daemon.
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
- - **Provenance attestations** on every npm release via GitHub Actions trusted publishing.
60
- - **Signed standalone binaries** from GitHub Releases for users who'd rather not touch npm.
61
- - **Reproducible builds + SBOM** published with each release.
62
- - **No long-lived publish tokens.** OIDC only.
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 `sigild`'s address space.
17
- 2. **Signing authority.** A signature `sigild` produces on behalf of a portal is a financial action. The set of authorized actions is bounded by per-portal policy.
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 `sigild`'s memory only, unlocked from encrypted at-rest storage. Plaintext is zeroized on shutdown. mlock against swap is planned (requires a native module distributed as bundled prebuilds, see Known limitations).
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
- Defenses (policy engine, per portal):
37
- - Destination allowlists (`allow_to`)
38
- - Per-tx and rolling-window value caps
39
- - Allowed function selectors (only `transfer`, `approve`, etc.)
40
- - Chain ID allowlists
41
- - Out-of-band human confirmation above a configurable value threshold (push to Pushover / Telegram / Apple Push)
42
- - Append-only audit log with monotonic counter
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` the daemon, read mlock'd memory, or simply ask `sigild` to sign things over the socket. `sigil` makes this harder (audit log will show it, policy still applies) but does not prevent it.
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 currently unauthenticated within the user's session. Any process running as `$USER` can connect to it. This is consistent with the threat model (we don't defend against local user compromise) but worth stating explicitly.
79
- - **mlock is not yet implemented.** Plaintext key material lives in a regular `Buffer` that is zeroized on daemon shutdown or unlock-failure. 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.
80
- - Out-of-band confirmation depends on a working push channel. If the push provider is down, high-value signs are denied, not approved.
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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=sigil-hook-post.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=sigil-hook-pre.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=sigil-mcp.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=sigil.d.ts.map
@@ -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"}