@provos/ironcurtain 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -100
- package/dist/auth/auth-command.d.ts +15 -0
- package/dist/auth/auth-command.js +424 -0
- package/dist/auth/auth-command.js.map +1 -0
- package/dist/auth/oauth-flow.d.ts +46 -0
- package/dist/auth/oauth-flow.js +279 -0
- package/dist/auth/oauth-flow.js.map +1 -0
- package/dist/auth/oauth-provider.d.ts +55 -0
- package/dist/auth/oauth-provider.js +73 -0
- package/dist/auth/oauth-provider.js.map +1 -0
- package/dist/auth/oauth-registry.d.ts +34 -0
- package/dist/auth/oauth-registry.js +69 -0
- package/dist/auth/oauth-registry.js.map +1 -0
- package/dist/auth/oauth-token-provider.d.ts +52 -0
- package/dist/auth/oauth-token-provider.js +159 -0
- package/dist/auth/oauth-token-provider.js.map +1 -0
- package/dist/auth/oauth-token-store.d.ts +30 -0
- package/dist/auth/oauth-token-store.js +93 -0
- package/dist/auth/oauth-token-store.js.map +1 -0
- package/dist/auth/providers/google-scopes.d.ts +35 -0
- package/dist/auth/providers/google-scopes.js +183 -0
- package/dist/auth/providers/google-scopes.js.map +1 -0
- package/dist/auth/providers/google.d.ts +2 -0
- package/dist/auth/providers/google.js +28 -0
- package/dist/auth/providers/google.js.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.js +39 -1
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/constitution-readonly.md +14 -11
- package/dist/config/constitution-user-base.md +15 -0
- package/dist/config/first-start.js +4 -1
- package/dist/config/first-start.js.map +1 -1
- package/dist/config/generated/test-scenarios.json +1 -1
- package/dist/config/generated/tool-annotations.json +10617 -296
- package/dist/config/generated-readonly/compiled-policy.json +787 -244
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/mcp-servers.json +31 -2
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +43 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/types.d.ts +8 -0
- package/dist/config/user-config.d.ts +29 -0
- package/dist/config/user-config.js +30 -0
- package/dist/config/user-config.js.map +1 -1
- package/dist/cron/constitution-generator.d.ts +2 -1
- package/dist/cron/constitution-generator.js +35 -36
- package/dist/cron/constitution-generator.js.map +1 -1
- package/dist/cron/headless-transport.d.ts +4 -0
- package/dist/cron/headless-transport.js +1 -1
- package/dist/cron/headless-transport.js.map +1 -1
- package/dist/daemon/ironcurtain-daemon.js +7 -1
- package/dist/daemon/ironcurtain-daemon.js.map +1 -1
- package/dist/docker/adapters/claude-code.js +32 -5
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +40 -0
- package/dist/docker/claude-md-seed.d.ts +27 -0
- package/dist/docker/claude-md-seed.js +42 -0
- package/dist/docker/claude-md-seed.js.map +1 -0
- package/dist/docker/docker-agent-session.d.ts +3 -1
- package/dist/docker/docker-agent-session.js +51 -28
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +15 -1
- package/dist/docker/docker-infrastructure.js +105 -9
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.d.ts +2 -0
- package/dist/docker/docker-manager.js +9 -1
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +65 -0
- package/dist/docker/mitm-proxy.js +288 -31
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/package-types.d.ts +127 -0
- package/dist/docker/package-types.js +8 -0
- package/dist/docker/package-types.js.map +1 -0
- package/dist/docker/package-validator.d.ts +41 -0
- package/dist/docker/package-validator.js +90 -0
- package/dist/docker/package-validator.js.map +1 -0
- package/dist/docker/platform.d.ts +6 -6
- package/dist/docker/platform.js +8 -6
- package/dist/docker/platform.js.map +1 -1
- package/dist/docker/proxy-tools.d.ts +80 -0
- package/dist/docker/proxy-tools.js +248 -0
- package/dist/docker/proxy-tools.js.map +1 -0
- package/dist/docker/pty-session.d.ts +15 -0
- package/dist/docker/pty-session.js +253 -73
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/docker/pty-types.d.ts +28 -0
- package/dist/docker/pty-types.js +2 -0
- package/dist/docker/pty-types.js.map +1 -1
- package/dist/docker/registry-proxy.d.ts +142 -0
- package/dist/docker/registry-proxy.js +856 -0
- package/dist/docker/registry-proxy.js.map +1 -0
- package/dist/escalation/escalation-watcher.d.ts +3 -1
- package/dist/escalation/escalation-watcher.js +6 -2
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/escalation/listener-command.js +24 -10
- package/dist/escalation/listener-command.js.map +1 -1
- package/dist/escalation/listener-lock.d.ts +5 -0
- package/dist/escalation/listener-lock.js +14 -0
- package/dist/escalation/listener-lock.js.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/memory/auto-save.d.ts +25 -0
- package/dist/memory/auto-save.js +89 -0
- package/dist/memory/auto-save.js.map +1 -0
- package/dist/memory/memory-annotations.d.ts +47 -0
- package/dist/memory/memory-annotations.js +87 -0
- package/dist/memory/memory-annotations.js.map +1 -0
- package/dist/memory/memory-prompt.d.ts +12 -0
- package/dist/memory/memory-prompt.js +20 -0
- package/dist/memory/memory-prompt.js.map +1 -0
- package/dist/memory/resolve-memory-path.d.ts +20 -0
- package/dist/memory/resolve-memory-path.js +30 -0
- package/dist/memory/resolve-memory-path.js.map +1 -0
- package/dist/mux/mux-app.d.ts +4 -0
- package/dist/mux/mux-app.js +246 -28
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.d.ts +2 -2
- package/dist/mux/mux-command.js +14 -22
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-escalation-manager.d.ts +9 -3
- package/dist/mux/mux-escalation-manager.js +69 -19
- package/dist/mux/mux-escalation-manager.js.map +1 -1
- package/dist/mux/mux-input-handler.d.ts +59 -3
- package/dist/mux/mux-input-handler.js +270 -32
- package/dist/mux/mux-input-handler.js.map +1 -1
- package/dist/mux/mux-renderer.d.ts +19 -1
- package/dist/mux/mux-renderer.js +466 -11
- package/dist/mux/mux-renderer.js.map +1 -1
- package/dist/mux/persona-scanner.d.ts +19 -0
- package/dist/mux/persona-scanner.js +53 -0
- package/dist/mux/persona-scanner.js.map +1 -0
- package/dist/mux/pty-bridge.d.ts +8 -0
- package/dist/mux/pty-bridge.js +19 -1
- package/dist/mux/pty-bridge.js.map +1 -1
- package/dist/mux/session-scanner.d.ts +18 -0
- package/dist/mux/session-scanner.js +65 -0
- package/dist/mux/session-scanner.js.map +1 -0
- package/dist/mux/types.d.ts +36 -1
- package/dist/mux/types.js +14 -1
- package/dist/mux/types.js.map +1 -1
- package/dist/persona/persona-command.js +130 -93
- package/dist/persona/persona-command.js.map +1 -1
- package/dist/persona/persona-prompt.d.ts +7 -7
- package/dist/persona/persona-prompt.js +14 -29
- package/dist/persona/persona-prompt.js.map +1 -1
- package/dist/persona/resolve.js +5 -2
- package/dist/persona/resolve.js.map +1 -1
- package/dist/pipeline/annotate.js +1 -7
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.d.ts +5 -2
- package/dist/pipeline/compile.js +23 -11
- package/dist/pipeline/compile.js.map +1 -1
- package/dist/pipeline/constitution-compiler.d.ts +302 -3
- package/dist/pipeline/constitution-compiler.js +285 -23
- package/dist/pipeline/constitution-compiler.js.map +1 -1
- package/dist/pipeline/generate-with-repair.d.ts +2 -2
- package/dist/pipeline/generate-with-repair.js +2 -2
- package/dist/pipeline/handwritten-scenarios.d.ts +20 -7
- package/dist/pipeline/handwritten-scenarios.js +20 -41
- package/dist/pipeline/handwritten-scenarios.js.map +1 -1
- package/dist/pipeline/list-resolver.d.ts +18 -20
- package/dist/pipeline/list-resolver.js +28 -36
- package/dist/pipeline/list-resolver.js.map +1 -1
- package/dist/pipeline/pipeline-runner.d.ts +102 -13
- package/dist/pipeline/pipeline-runner.js +497 -237
- package/dist/pipeline/pipeline-runner.js.map +1 -1
- package/dist/pipeline/pipeline-shared.d.ts +7 -9
- package/dist/pipeline/pipeline-shared.js +8 -18
- package/dist/pipeline/pipeline-shared.js.map +1 -1
- package/dist/pipeline/policy-verifier.d.ts +32 -5
- package/dist/pipeline/policy-verifier.js +137 -8
- package/dist/pipeline/policy-verifier.js.map +1 -1
- package/dist/pipeline/proxy-mcp-connections.d.ts +25 -0
- package/dist/pipeline/proxy-mcp-connections.js +121 -0
- package/dist/pipeline/proxy-mcp-connections.js.map +1 -0
- package/dist/pipeline/refresh-lists.js +21 -13
- package/dist/pipeline/refresh-lists.js.map +1 -1
- package/dist/pipeline/scenario-generator.d.ts +29 -49
- package/dist/pipeline/scenario-generator.js +180 -143
- package/dist/pipeline/scenario-generator.js.map +1 -1
- package/dist/pipeline/scenario-schema-validator.d.ts +51 -0
- package/dist/pipeline/scenario-schema-validator.js +132 -0
- package/dist/pipeline/scenario-schema-validator.js.map +1 -0
- package/dist/pipeline/tool-annotator.d.ts +9 -8
- package/dist/pipeline/tool-annotator.js +48 -95
- package/dist/pipeline/tool-annotator.js.map +1 -1
- package/dist/pipeline/types.d.ts +31 -24
- package/dist/sandbox/index.d.ts +26 -10
- package/dist/sandbox/index.js +151 -40
- package/dist/sandbox/index.js.map +1 -1
- package/dist/servers/fetch-server.js +10 -5
- package/dist/servers/fetch-server.js.map +1 -1
- package/dist/session/agent-session.d.ts +8 -20
- package/dist/session/agent-session.js +22 -70
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/base-transport.d.ts +17 -1
- package/dist/session/base-transport.js +25 -1
- package/dist/session/base-transport.js.map +1 -1
- package/dist/session/cli-transport.d.ts +5 -0
- package/dist/session/cli-transport.js +48 -25
- package/dist/session/cli-transport.js.map +1 -1
- package/dist/session/index.d.ts +25 -1
- package/dist/session/index.js +57 -8
- package/dist/session/index.js.map +1 -1
- package/dist/session/prompts.js +8 -9
- package/dist/session/prompts.js.map +1 -1
- package/dist/session/session-manager.d.ts +6 -0
- package/dist/session/session-manager.js +12 -0
- package/dist/session/session-manager.js.map +1 -1
- package/dist/session/types.d.ts +11 -1
- package/dist/session/types.js.map +1 -1
- package/dist/session/workspace-validation.js +37 -6
- package/dist/session/workspace-validation.js.map +1 -1
- package/dist/signal/signal-bot-daemon.js +24 -8
- package/dist/signal/signal-bot-daemon.js.map +1 -1
- package/dist/signal/signal-transport.d.ts +5 -2
- package/dist/signal/signal-transport.js +3 -3
- package/dist/signal/signal-transport.js.map +1 -1
- package/dist/trusted-process/approval-whitelist.d.ts +117 -0
- package/dist/trusted-process/approval-whitelist.js +237 -0
- package/dist/trusted-process/approval-whitelist.js.map +1 -0
- package/dist/trusted-process/gworkspace-credentials.d.ts +40 -0
- package/dist/trusted-process/gworkspace-credentials.js +45 -0
- package/dist/trusted-process/gworkspace-credentials.js.map +1 -0
- package/dist/trusted-process/index.d.ts +11 -1
- package/dist/trusted-process/index.js +83 -53
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.d.ts +23 -0
- package/dist/trusted-process/mcp-proxy-server.js +409 -94
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/policy-engine.d.ts +12 -3
- package/dist/trusted-process/policy-engine.js +33 -23
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/policy-types.d.ts +17 -0
- package/dist/trusted-process/sandbox-integration.d.ts +23 -0
- package/dist/trusted-process/sandbox-integration.js +115 -3
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/dist/trusted-process/token-file-refresher.d.ts +69 -0
- package/dist/trusted-process/token-file-refresher.js +120 -0
- package/dist/trusted-process/token-file-refresher.js.map +1 -0
- package/dist/types/argument-roles.d.ts +7 -1
- package/dist/types/argument-roles.js +50 -0
- package/dist/types/argument-roles.js.map +1 -1
- package/dist/types/audit.d.ts +11 -0
- package/docker/Dockerfile.base +22 -0
- package/docker/Dockerfile.base.arm64 +44 -4
- package/docker/Dockerfile.claude-code +2 -2
- package/docker/entrypoint-claude-code.sh +24 -6
- package/package.json +8 -2
- package/src/config/constitution-user-base.md +15 -0
- package/src/config/generated/test-scenarios.json +1 -1
- package/src/config/generated/tool-annotations.json +10617 -296
- package/src/config/mcp-servers.json +31 -2
- package/dist/pipeline/mcp-connections.d.ts +0 -16
- package/dist/pipeline/mcp-connections.js +0 -61
- package/dist/pipeline/mcp-connections.js.map +0 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ _\*When someone writes "secure," you should immediately be skeptical. [What do w
|
|
|
18
18
|
<img src="demo.gif" alt="IronCurtain mux demo: trusted input from command mode enables auto-approval of git clone and git push" width="800">
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
|
-
The agent is asked to clone a repository and push changes. Both `git_clone` and `git_push` are escalated by the policy engine, but the
|
|
21
|
+
The agent is asked to clone a repository and push changes. Both `git_clone` and `git_push` are escalated by the policy engine, but the auto-approver approves them automatically — the user's trusted input from command mode (Ctrl-A) provided clear intent, so no manual `/approve` was needed.
|
|
22
22
|
|
|
23
23
|
## The Problem
|
|
24
24
|
|
|
@@ -45,7 +45,7 @@ IronCurtain supports two session modes with different trust models:
|
|
|
45
45
|
|
|
46
46
|
- **Builtin Agent (Code Mode)** — IronCurtain's own LLM agent writes TypeScript snippets that execute in a V8 sandbox. IronCurtain controls the agent, the sandbox, and the policy engine. Every tool call exits the sandbox as a structured MCP request, passes through the policy engine (allow / deny / escalate), and only then reaches the real MCP server.
|
|
47
47
|
|
|
48
|
-
- **Docker Agent Mode** — An external agent (Claude Code, Goose, etc.) runs inside a Docker container with no network access. IronCurtain mediates the external effects: LLM API calls pass through a TLS-terminating MITM proxy (host allowlist, fake-to-real key swap),
|
|
48
|
+
- **Docker Agent Mode** — An external agent (Claude Code, Goose, etc.) runs inside a Docker container with no network access. IronCurtain mediates the external effects: LLM API calls pass through a TLS-terminating MITM proxy (host allowlist, fake-to-real key swap), MCP tool calls pass through the same policy engine, and package installations (npm/PyPI) go through a validating registry proxy.
|
|
49
49
|
|
|
50
50
|
In both modes, the agent is **untrusted**. Security does not depend on the model following instructions — it is enforced at the boundary.
|
|
51
51
|
|
|
@@ -109,7 +109,7 @@ ironcurtain mux
|
|
|
109
109
|
|
|
110
110
|
- **Full agent TUI** — The agent runs in a PTY inside a Docker container with no network access. You interact with it exactly as if it were running locally.
|
|
111
111
|
- **Inline escalation handling** — When a tool call needs approval, an escalation panel overlays the viewport. Press Ctrl-A to enter command mode, `/approve N` or `/deny N`, and return.
|
|
112
|
-
- **Trusted user input** — Text typed in command mode (Ctrl-A) is captured on the host side before entering the container. This creates a verified intent signal that the
|
|
112
|
+
- **Trusted user input** — Text typed in command mode (Ctrl-A) is captured on the host side before entering the container. This creates a verified intent signal that the auto-approver can use — e.g., typing "push my changes to origin" will auto-approve a subsequent `git_push` escalation.
|
|
113
113
|
- **Tab management** — Spawn multiple concurrent sessions (`/new`), switch between them (`/tab N`, Alt-1..9), close them (`/close`).
|
|
114
114
|
|
|
115
115
|
See [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md) for the full walkthrough: input modes, trusted input security model, escalation workflow, and keyboard reference.
|
|
@@ -122,11 +122,12 @@ For quick tasks or environments without Docker, IronCurtain's builtin agent runs
|
|
|
122
122
|
ironcurtain start # Interactive multi-turn session
|
|
123
123
|
ironcurtain start "Summarize the files in ./src" # Single-shot mode
|
|
124
124
|
ironcurtain start -w ./my-project "Fix the tests" # Workspace mode
|
|
125
|
+
ironcurtain start --persona my-assistant "Check my email" # Use a persona
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
### Other running modes
|
|
128
129
|
|
|
129
|
-
IronCurtain also supports PTY mode
|
|
130
|
+
IronCurtain also supports PTY mode, session resume (`--resume <session-id>`), a Signal messaging transport for mobile approval, and a daemon mode for scheduled cron jobs. See [RUNNING_MODES.md](RUNNING_MODES.md) for details.
|
|
130
131
|
|
|
131
132
|
## Customizing Your Policy
|
|
132
133
|
|
|
@@ -148,6 +149,18 @@ ironcurtain compile-policy
|
|
|
148
149
|
|
|
149
150
|
Translates your constitution into deterministic rules, generates test scenarios, and verifies them. Compiled artifacts go to `~/.ironcurtain/generated/`.
|
|
150
151
|
|
|
152
|
+
### Personas
|
|
153
|
+
|
|
154
|
+
Personas are named policy profiles — each bundles a constitution, compiled policy, persistent workspace, and semantic memory. Use them to run agents with different roles or access levels.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
ironcurtain persona create my-assistant # Create a persona
|
|
158
|
+
ironcurtain persona compile my-assistant # Compile its policy
|
|
159
|
+
ironcurtain start --persona my-assistant "Check my calendar"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
In mux mode, `/new my-assistant` spawns a tab using that persona. Personas can also be assigned to cron jobs. See [DAEMON.md](DAEMON.md) for scheduled job configuration.
|
|
163
|
+
|
|
151
164
|
## Policy: Constitution → Enforcement
|
|
152
165
|
|
|
153
166
|
You write intent in plain English; IronCurtain compiles it into deterministic rules:
|
|
@@ -198,27 +211,6 @@ ironcurtain refresh-lists --list major-news # Refresh a single list
|
|
|
198
211
|
|
|
199
212
|
Review the generated `~/.ironcurtain/generated/compiled-policy.json` — these are the exact rules enforced at runtime.
|
|
200
213
|
|
|
201
|
-
## Web Search
|
|
202
|
-
|
|
203
|
-
IronCurtain's fetch server includes a `web_search` tool backed by your choice of provider. Configure via `ironcurtain config` or directly in `~/.ironcurtain/config.json`:
|
|
204
|
-
|
|
205
|
-
```json
|
|
206
|
-
{
|
|
207
|
-
"webSearch": {
|
|
208
|
-
"provider": "brave",
|
|
209
|
-
"brave": { "apiKey": "BSA..." }
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
| Provider | `provider` value | API key field | Sign up |
|
|
215
|
-
| -------- | ---------------- | -------------------------- | ----------------------------- |
|
|
216
|
-
| Brave | `"brave"` | `webSearch.brave.apiKey` | https://brave.com/search/api/ |
|
|
217
|
-
| Tavily | `"tavily"` | `webSearch.tavily.apiKey` | https://tavily.com/ |
|
|
218
|
-
| SerpAPI | `"serpapi"` | `webSearch.serpapi.apiKey` | https://serpapi.com/ |
|
|
219
|
-
|
|
220
|
-
`web_search` is available in both builtin and Docker Agent modes. If no provider is configured, calls to `web_search` return an error explaining how to set one up via `ironcurtain config`.
|
|
221
|
-
|
|
222
214
|
## Configuration
|
|
223
215
|
|
|
224
216
|
IronCurtain stores configuration and session data in `~/.ironcurtain/`:
|
|
@@ -229,8 +221,7 @@ IronCurtain stores configuration and session data in `~/.ironcurtain/`:
|
|
|
229
221
|
├── constitution.md # User-local base constitution (overrides package default)
|
|
230
222
|
├── constitution-user.md # Your policy customizations (generated by customize-policy)
|
|
231
223
|
├── generated/ # User-compiled policy artifacts (overrides package defaults)
|
|
232
|
-
├──
|
|
233
|
-
├── pty-registry/ # Active PTY session registrations (auto-managed)
|
|
224
|
+
├── personas/ # Persona directories (constitution, policy, workspace, memory)
|
|
234
225
|
├── jobs/ # Cron job definitions, workspaces, and run records
|
|
235
226
|
├── sessions/
|
|
236
227
|
│ └── {sessionId}/
|
|
@@ -246,84 +237,21 @@ Edit configuration interactively:
|
|
|
246
237
|
ironcurtain config
|
|
247
238
|
```
|
|
248
239
|
|
|
249
|
-
See [CONFIG.md](CONFIG.md) for the full
|
|
250
|
-
|
|
251
|
-
### Resource Budgets
|
|
252
|
-
|
|
253
|
-
Sessions enforce configurable limits to prevent runaway agents:
|
|
254
|
-
|
|
255
|
-
| Limit | Default | Config Key |
|
|
256
|
-
| --------------- | ---------- | ------------------------------------ |
|
|
257
|
-
| Max tokens | 1,000,000 | `resourceBudget.maxTotalTokens` |
|
|
258
|
-
| Max steps | 200 | `resourceBudget.maxSteps` |
|
|
259
|
-
| Session timeout | 30 minutes | `resourceBudget.maxSessionSeconds` |
|
|
260
|
-
| Cost cap | $5.00 | `resourceBudget.maxEstimatedCostUsd` |
|
|
261
|
-
|
|
262
|
-
Set any limit to `null` in `config.json` to disable it.
|
|
263
|
-
|
|
264
|
-
### Auto-Approve Escalations
|
|
265
|
-
|
|
266
|
-
By default, all escalations require manual `/approve` or `/deny`. You can optionally enable an LLM-based auto-approver that checks whether the user's most recent message clearly authorized the escalated action:
|
|
267
|
-
|
|
268
|
-
```json
|
|
269
|
-
{
|
|
270
|
-
"autoApprove": {
|
|
271
|
-
"enabled": true,
|
|
272
|
-
"modelId": "anthropic:claude-haiku-4-5"
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
The auto-approver is conservative — it only approves when intent is unambiguous (e.g., "push my changes to origin" clearly authorizes `git_push`). Vague messages like "go ahead" or "fix the tests" always fall through to human review. It can never deny — only approve or escalate. All auto-approved actions are recorded in the audit log with `autoApproved: true`.
|
|
278
|
-
|
|
279
|
-
### Audit Redaction
|
|
280
|
-
|
|
281
|
-
Audit log entries may contain sensitive data passed through tool arguments or results. By default, recognized patterns (credit card numbers, SSNs, API keys) are automatically redacted before entries are written to `audit.jsonl`. Disable with `"auditRedaction": { "enabled": false }` in `config.json` for full forensic logging.
|
|
282
|
-
|
|
283
|
-
### Multi-Provider Support
|
|
284
|
-
|
|
285
|
-
IronCurtain supports multiple LLM providers. Use the `provider:model-name` format in config and provide the API key for each provider you use:
|
|
286
|
-
|
|
287
|
-
```json
|
|
288
|
-
{
|
|
289
|
-
"agentModelId": "anthropic:claude-sonnet-4-6",
|
|
290
|
-
"policyModelId": "google:gemini-2.5-flash",
|
|
291
|
-
"googleApiKey": "AIza..."
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
| Provider | Config Key | Environment Variable |
|
|
296
|
-
| --------- | ----------------- | ------------------------------ |
|
|
297
|
-
| Anthropic | `anthropicApiKey` | `ANTHROPIC_API_KEY` |
|
|
298
|
-
| Google | `googleApiKey` | `GOOGLE_GENERATIVE_AI_API_KEY` |
|
|
299
|
-
| OpenAI | `openaiApiKey` | `OPENAI_API_KEY` |
|
|
300
|
-
|
|
301
|
-
Environment variables take precedence over config file values.
|
|
302
|
-
|
|
303
|
-
### Adding MCP Servers
|
|
304
|
-
|
|
305
|
-
IronCurtain ships with filesystem, git, fetch, and GitHub MCP servers pre-configured. Adding a new server is a developer-level task:
|
|
306
|
-
|
|
307
|
-
1. **Register the server** in `src/config/mcp-servers.json` with its command, arguments, and optional environment variables or sandbox settings.
|
|
308
|
-
2. **Extend the argument role registry** in `src/types/argument-roles.ts` if the new server's tools have argument semantics not covered by existing roles (e.g., `read-path`, `write-path`, `fetch-url`).
|
|
309
|
-
3. **Update the constitution** in `src/config/constitution.md` to cover the new server's capabilities.
|
|
310
|
-
4. **Re-run `ironcurtain annotate-tools`** to classify the new server's tool arguments by role.
|
|
311
|
-
5. **Re-run `ironcurtain compile-policy`** to compile policy rules from your constitution. The verification stage will flag gaps.
|
|
312
|
-
|
|
313
|
-
After compilation, review the updated `tool-annotations.json` and `compiled-policy.json` to verify the new tools are correctly classified and covered by policy.
|
|
240
|
+
Key configuration areas: models and API keys, resource budgets (token/step/time/cost limits), auto-approve escalations, web search provider, audit redaction, and memory server LLM settings. See [CONFIG.md](CONFIG.md) for the full reference.
|
|
314
241
|
|
|
315
242
|
## Built-in Capabilities
|
|
316
243
|
|
|
317
|
-
IronCurtain ships with
|
|
244
|
+
IronCurtain ships with five pre-configured MCP servers. All tool calls (except memory) are governed by your compiled policy.
|
|
318
245
|
|
|
319
246
|
| Server | Tools | Key capabilities |
|
|
320
247
|
| -------------- | ----- | ----------------------------------------------------------------------------------------------------------------- |
|
|
321
248
|
| **Filesystem** | 14 | Read, write, edit, search files; directory tree; move; diff calculation |
|
|
322
249
|
| **Git** | 27 | Full git workflow: status, diff, log, commit, branch, push/pull/fetch, clone, stash, blame |
|
|
323
|
-
| **Fetch** | 2 | HTTP GET with HTML-to-markdown conversion;
|
|
250
|
+
| **Fetch** | 2 | HTTP GET with HTML-to-markdown conversion; web search (Brave, Tavily, SerpAPI) |
|
|
324
251
|
| **GitHub** | 41 | Issues, PRs, code search, reviews via `ghcr.io/github/github-mcp-server`; requires a GitHub personal access token |
|
|
252
|
+
| **Memory** | 5 | Persistent semantic memory with hybrid vector+keyword search, LLM summarization, and automatic compaction. Enabled for persona and cron sessions. |
|
|
325
253
|
|
|
326
|
-
Read-only operations are allowed by default policy; mutations (writes, pushes, PR creation) escalate for human approval.
|
|
254
|
+
Read-only operations are allowed by default policy; mutations (writes, pushes, PR creation) escalate for human approval. Tools use `server.tool` naming (e.g., `filesystem.read_file`, `memory.recall`).
|
|
327
255
|
|
|
328
256
|
## Security Model
|
|
329
257
|
|
|
@@ -334,7 +262,7 @@ IronCurtain is designed around a specific threat model: **the LLM goes rogue.**
|
|
|
334
262
|
- **Filesystem containment** — Symlink-aware path resolution prevents path traversal and symlink-escape attacks.
|
|
335
263
|
- **Per-tool policy** — Each MCP tool call is evaluated against compiled rules. The policy engine classifies tool arguments by role (read-path, write-path, delete-path) to make fine-grained decisions.
|
|
336
264
|
- **Structural invariants** — Certain protections are hardcoded and cannot be overridden by the constitution: the agent can never modify its own policy files, audit logs, or configuration.
|
|
337
|
-
- **Human escalation** — When policy says "escalate," the agent pauses and the user must explicitly approve or deny. Optionally, an
|
|
265
|
+
- **Human escalation** — When policy says "escalate," the agent pauses and the user must explicitly approve or deny. Optionally, an LLM-based auto-approver handles unambiguous cases (see [CONFIG.md](CONFIG.md)).
|
|
338
266
|
- **Audit trail** — Every tool call and policy decision is logged to an append-only JSONL audit log.
|
|
339
267
|
- **Resource limits** — Token, step, time, and cost budgets prevent runaway sessions.
|
|
340
268
|
|
|
@@ -382,21 +310,22 @@ src/
|
|
|
382
310
|
├── index.ts # Entry point
|
|
383
311
|
├── cli.ts # CLI command dispatcher
|
|
384
312
|
├── config/ # Configuration loading, constitution, MCP server definitions
|
|
385
|
-
│ ├── constitution.md # Base security policy in plain English
|
|
386
|
-
│ ├── mcp-servers.json # MCP server definitions
|
|
387
|
-
│ └── generated/ # Compiled policy artifacts (do not edit manually)
|
|
388
313
|
├── session/ # Multi-turn session management, budgets, loop detection
|
|
389
314
|
├── sandbox/ # V8 isolated execution environment
|
|
390
315
|
├── trusted-process/ # Policy engine, MCP proxy, audit log, escalation handler
|
|
391
316
|
├── pipeline/ # Constitution → policy compilation pipeline
|
|
392
317
|
├── escalation/ # Escalation listener: session registry, TUI dashboard, state
|
|
393
318
|
├── mux/ # Terminal multiplexer: PTY bridge, renderer, trusted input
|
|
319
|
+
├── persona/ # Persona management (create, compile, resolve)
|
|
320
|
+
├── memory/ # Memory server integration (config, annotations, path resolution)
|
|
394
321
|
├── signal/ # Signal messaging transport (bot daemon, setup, formatting)
|
|
395
322
|
├── daemon/ # Unified daemon (Signal + cron scheduler, control socket)
|
|
396
323
|
├── cron/ # Cron job management (scheduler, job store, git sync, policy)
|
|
397
|
-
├── docker/ # Docker agent mode, PTY session, MITM proxy,
|
|
324
|
+
├── docker/ # Docker agent mode, PTY session, MITM proxy, registry proxy
|
|
398
325
|
├── servers/ # Built-in MCP servers (fetch, web search providers)
|
|
399
326
|
└── types/ # Shared type definitions
|
|
327
|
+
packages/
|
|
328
|
+
└── memory-mcp-server/ # Standalone memory MCP server (publishable npm package)
|
|
400
329
|
```
|
|
401
330
|
|
|
402
331
|
## License
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point for `ironcurtain auth` subcommands.
|
|
3
|
+
*
|
|
4
|
+
* Manages third-party OAuth providers: credential import, authorization,
|
|
5
|
+
* status checking, and token revocation.
|
|
6
|
+
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* ironcurtain auth - show status of all providers
|
|
9
|
+
* ironcurtain auth import <provider> <file> - import OAuth client credentials
|
|
10
|
+
* ironcurtain auth <provider> - authorize a provider (opens browser)
|
|
11
|
+
* ironcurtain auth <provider> --scopes x,y - authorize with additional scopes
|
|
12
|
+
* ironcurtain auth status - same as no args
|
|
13
|
+
* ironcurtain auth revoke <provider> - revoke and delete stored token
|
|
14
|
+
*/
|
|
15
|
+
export declare function runAuthCommand(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point for `ironcurtain auth` subcommands.
|
|
3
|
+
*
|
|
4
|
+
* Manages third-party OAuth providers: credential import, authorization,
|
|
5
|
+
* status checking, and token revocation.
|
|
6
|
+
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* ironcurtain auth - show status of all providers
|
|
9
|
+
* ironcurtain auth import <provider> <file> - import OAuth client credentials
|
|
10
|
+
* ironcurtain auth <provider> - authorize a provider (opens browser)
|
|
11
|
+
* ironcurtain auth <provider> --scopes x,y - authorize with additional scopes
|
|
12
|
+
* ironcurtain auth status - same as no args
|
|
13
|
+
* ironcurtain auth revoke <provider> - revoke and delete stored token
|
|
14
|
+
*/
|
|
15
|
+
import { copyFileSync, existsSync, chmodSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
import { parseArgs } from 'node:util';
|
|
18
|
+
import * as p from '@clack/prompts';
|
|
19
|
+
import { printHelp } from '../cli-help.js';
|
|
20
|
+
import { getOAuthDir, getOAuthTokenPath } from '../config/paths.js';
|
|
21
|
+
import { loadClientCredentials } from './oauth-provider.js';
|
|
22
|
+
import { getAllOAuthProviders, printAvailableProviders, resolveProviderOrExit } from './oauth-registry.js';
|
|
23
|
+
import { runOAuthFlow, fetchWithTimeout } from './oauth-flow.js';
|
|
24
|
+
import { deleteOAuthToken, loadOAuthToken, saveOAuthToken } from './oauth-token-store.js';
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Help specs
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const authSpec = {
|
|
29
|
+
name: 'ironcurtain auth',
|
|
30
|
+
description: 'Manage third-party OAuth providers for MCP servers',
|
|
31
|
+
usage: [
|
|
32
|
+
'ironcurtain auth # Show provider status',
|
|
33
|
+
'ironcurtain auth import <provider> <file> # Import client credentials',
|
|
34
|
+
'ironcurtain auth <provider> # Authorize (opens browser)',
|
|
35
|
+
'ironcurtain auth <provider> --scopes <scopes> # Authorize with extra scopes',
|
|
36
|
+
'ironcurtain auth revoke <provider> # Revoke stored token',
|
|
37
|
+
'ironcurtain auth status # Show provider status',
|
|
38
|
+
],
|
|
39
|
+
subcommands: [
|
|
40
|
+
{ name: 'import <provider> <file>', description: 'Import OAuth client credentials from provider' },
|
|
41
|
+
{ name: '<provider>', description: 'Authorize a provider (opens browser for OAuth flow)' },
|
|
42
|
+
{ name: 'revoke <provider>', description: 'Revoke and delete stored OAuth token' },
|
|
43
|
+
{ name: 'status', description: 'Show authorization status for all providers' },
|
|
44
|
+
],
|
|
45
|
+
options: [
|
|
46
|
+
{ flag: 'scopes', description: 'Comma-separated scopes for incremental consent', placeholder: '<scopes>' },
|
|
47
|
+
{ flag: 'help', short: 'h', description: 'Show this help message' },
|
|
48
|
+
],
|
|
49
|
+
examples: [
|
|
50
|
+
'ironcurtain auth import google ~/Downloads/credentials.json',
|
|
51
|
+
'ironcurtain auth google',
|
|
52
|
+
'ironcurtain auth google --scopes gmail.send,calendar.events',
|
|
53
|
+
'ironcurtain auth revoke google',
|
|
54
|
+
'ironcurtain auth status',
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Google Cloud setup guide (shown when credentials are missing)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
const GOOGLE_SETUP_GUIDE = `
|
|
61
|
+
Google Cloud Project Setup
|
|
62
|
+
==========================
|
|
63
|
+
|
|
64
|
+
IronCurtain requires you to create your own Google Cloud OAuth credentials.
|
|
65
|
+
This is a one-time setup.
|
|
66
|
+
|
|
67
|
+
Step 1: Create a Google Cloud Project
|
|
68
|
+
Go to https://console.cloud.google.com/projectcreate
|
|
69
|
+
Enter a project name (e.g., "IronCurtain") and click Create.
|
|
70
|
+
|
|
71
|
+
Step 2: Enable Google Workspace APIs
|
|
72
|
+
Navigate to APIs & Services > Library and enable:
|
|
73
|
+
- Gmail API
|
|
74
|
+
- Google Calendar API
|
|
75
|
+
- Google Drive API
|
|
76
|
+
|
|
77
|
+
Step 3: Configure the OAuth Consent Screen
|
|
78
|
+
Navigate to APIs & Services > OAuth consent screen
|
|
79
|
+
Set User type to "External" (or "Internal" for Google Workspace orgs)
|
|
80
|
+
Fill in App name, support email, and developer contact email.
|
|
81
|
+
Add scopes based on what you need:
|
|
82
|
+
Read-only (default): gmail.readonly, calendar.readonly, drive.readonly
|
|
83
|
+
Write access: gmail.send, calendar.events, drive.file
|
|
84
|
+
You choose which scopes to enable — write scopes are optional.
|
|
85
|
+
Under Test users, add your Google account email.
|
|
86
|
+
|
|
87
|
+
Note: In "Testing" mode, refresh tokens expire after 7 days.
|
|
88
|
+
You will need to re-authorize weekly.
|
|
89
|
+
|
|
90
|
+
To add write scopes after initial authorization:
|
|
91
|
+
ironcurtain auth google --scopes gmail.send,calendar.events
|
|
92
|
+
|
|
93
|
+
Step 4: Create OAuth Client Credentials
|
|
94
|
+
Navigate to APIs & Services > Credentials
|
|
95
|
+
Click "Create Credentials" > "OAuth client ID"
|
|
96
|
+
Select Application type: "Desktop app"
|
|
97
|
+
Enter a name (e.g., "IronCurtain CLI")
|
|
98
|
+
Click Create, then "Download JSON"
|
|
99
|
+
|
|
100
|
+
Step 5: Import into IronCurtain
|
|
101
|
+
ironcurtain auth import google /path/to/downloaded-credentials.json
|
|
102
|
+
|
|
103
|
+
For more details, see: docs/designs/third-party-oauth.md
|
|
104
|
+
`;
|
|
105
|
+
/** Provider-specific setup guides, keyed by provider ID. */
|
|
106
|
+
const SETUP_GUIDES = {
|
|
107
|
+
google: GOOGLE_SETUP_GUIDE,
|
|
108
|
+
};
|
|
109
|
+
function showSetupGuide(providerId) {
|
|
110
|
+
const guide = SETUP_GUIDES[providerId];
|
|
111
|
+
if (guide) {
|
|
112
|
+
process.stdout.write(guide);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function truncate(value, maxLen) {
|
|
116
|
+
return value.length > maxLen ? value.slice(0, maxLen) + '...' : value;
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Import credentials
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
/**
|
|
122
|
+
* Handles `ironcurtain auth import <provider> <credentials-file>`.
|
|
123
|
+
* Shows provider-specific setup guidance when credentials file is not provided.
|
|
124
|
+
*/
|
|
125
|
+
function importCredentials(args) {
|
|
126
|
+
const providerId = args[0];
|
|
127
|
+
const credentialsPath = args[1];
|
|
128
|
+
if (!providerId) {
|
|
129
|
+
process.stdout.write('Usage: ironcurtain auth import <provider> <credentials-file>\n\n');
|
|
130
|
+
printAvailableProviders();
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const provider = resolveProviderOrExit(providerId);
|
|
134
|
+
if (!credentialsPath) {
|
|
135
|
+
process.stdout.write(`Usage: ironcurtain auth import ${providerId} <credentials-file>\n`);
|
|
136
|
+
process.stdout.write('\nProvide the path to the credentials JSON file downloaded from your provider.\n');
|
|
137
|
+
showSetupGuide(provider.id);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const resolvedPath = resolve(credentialsPath);
|
|
142
|
+
// Ensure the oauth directory exists and copy the file
|
|
143
|
+
const oauthDir = getOAuthDir();
|
|
144
|
+
mkdirSync(oauthDir, { recursive: true });
|
|
145
|
+
const destPath = resolve(oauthDir, provider.credentialsFilename);
|
|
146
|
+
try {
|
|
147
|
+
copyFileSync(resolvedPath, destPath);
|
|
148
|
+
chmodSync(destPath, 0o600);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
+
process.stdout.write(`Failed to import credentials: ${message}\n`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
// Validate by loading the copied credentials
|
|
156
|
+
const credentials = loadClientCredentials(provider);
|
|
157
|
+
if (!credentials) {
|
|
158
|
+
process.stdout.write(`Invalid credentials file: could not extract client_id and client_secret.\n`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
process.stdout.write(`\n ${provider.displayName} -- Import OAuth Credentials\n\n`);
|
|
162
|
+
process.stdout.write(` Found client_id: ${truncate(credentials.clientId, 20)}\n`);
|
|
163
|
+
process.stdout.write(` Credentials saved to ${destPath}\n\n`);
|
|
164
|
+
process.stdout.write(` Next step: run 'ironcurtain auth ${provider.id}' to authorize.\n\n`);
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Status display
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
function showStatus() {
|
|
170
|
+
const providers = getAllOAuthProviders();
|
|
171
|
+
if (providers.length === 0) {
|
|
172
|
+
process.stdout.write('No OAuth providers registered.\n');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
process.stdout.write('\nOAuth Provider Status:\n\n');
|
|
176
|
+
for (const provider of providers) {
|
|
177
|
+
let credStatus;
|
|
178
|
+
try {
|
|
179
|
+
credStatus = loadClientCredentials(provider) !== null ? 'configured' : 'not configured';
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
credStatus = 'invalid';
|
|
183
|
+
}
|
|
184
|
+
const tokenPath = getOAuthTokenPath(provider.id);
|
|
185
|
+
const hasToken = existsSync(tokenPath);
|
|
186
|
+
const tokenStatus = hasToken ? 'authorized' : 'not authorized';
|
|
187
|
+
process.stdout.write(` ${provider.displayName} (${provider.id})\n`);
|
|
188
|
+
process.stdout.write(` Credentials: ${credStatus}\n`);
|
|
189
|
+
process.stdout.write(` Token: ${tokenStatus}\n`);
|
|
190
|
+
process.stdout.write('\n');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Revoke
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/**
|
|
197
|
+
* Attempts to revoke the token server-side via the provider's revocation
|
|
198
|
+
* endpoint (if configured), then deletes the local token file.
|
|
199
|
+
*/
|
|
200
|
+
async function revokeToken(providerId) {
|
|
201
|
+
const provider = resolveProviderOrExit(providerId);
|
|
202
|
+
const token = loadOAuthToken(provider.id);
|
|
203
|
+
if (!token) {
|
|
204
|
+
process.stdout.write(`No stored token found for ${provider.displayName}.\n`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// Attempt server-side revocation if the provider has a revocation endpoint
|
|
208
|
+
await revokeTokenRemotely(provider, token);
|
|
209
|
+
deleteOAuthToken(provider.id);
|
|
210
|
+
process.stdout.write(`Token revoked for ${provider.displayName}.\n`);
|
|
211
|
+
process.stdout.write(`Deleted: ${getOAuthTokenPath(provider.id)}\n`);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Calls the provider's token revocation endpoint to invalidate the token
|
|
215
|
+
* server-side. Best-effort: logs a warning on failure but does not throw.
|
|
216
|
+
*/
|
|
217
|
+
async function revokeTokenRemotely(provider, token) {
|
|
218
|
+
if (!provider.revocationUrl) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Prefer revoking the refresh token (invalidates both access and refresh)
|
|
222
|
+
const tokenToRevoke = token.refreshToken || token.accessToken;
|
|
223
|
+
const REVOCATION_TIMEOUT_MS = 10_000;
|
|
224
|
+
try {
|
|
225
|
+
const response = await fetchWithTimeout(provider.revocationUrl, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
228
|
+
body: new URLSearchParams({ token: tokenToRevoke }).toString(),
|
|
229
|
+
}, REVOCATION_TIMEOUT_MS);
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
const text = await response.text();
|
|
232
|
+
process.stdout.write(`Warning: Server-side revocation returned ${response.status}: ${text}\n`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
237
|
+
process.stdout.write(`Warning: Revocation request timed out after ${REVOCATION_TIMEOUT_MS}ms\n`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
241
|
+
process.stdout.write(`Warning: Could not reach revocation endpoint: ${message}\n`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Authorize
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
/**
|
|
249
|
+
* Parses optional --scopes flag from remaining args after the provider name.
|
|
250
|
+
* Returns the scopes array, or undefined if not specified.
|
|
251
|
+
*/
|
|
252
|
+
function parseScopesArg(args) {
|
|
253
|
+
// args is everything after the provider name
|
|
254
|
+
if (args.length === 0) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
const scopesFlagPresent = args.includes('--scopes');
|
|
258
|
+
try {
|
|
259
|
+
const { values } = parseArgs({
|
|
260
|
+
args,
|
|
261
|
+
options: {
|
|
262
|
+
scopes: { type: 'string' },
|
|
263
|
+
},
|
|
264
|
+
strict: false,
|
|
265
|
+
});
|
|
266
|
+
if (typeof values.scopes === 'string' && values.scopes.length > 0) {
|
|
267
|
+
return values.scopes
|
|
268
|
+
.split(',')
|
|
269
|
+
.map((s) => s.trim())
|
|
270
|
+
.filter((s) => s.length > 0);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (scopesFlagPresent) {
|
|
275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
276
|
+
throw new Error(`Failed to parse --scopes: ${message}`, { cause: err });
|
|
277
|
+
}
|
|
278
|
+
// --scopes not present: ignore unrelated parse errors
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Checks whether any of the selected scopes are non-default for this provider.
|
|
284
|
+
* Used to decide whether to show a consent-screen warning.
|
|
285
|
+
*/
|
|
286
|
+
function hasNonDefaultScopes(provider, scopes) {
|
|
287
|
+
const defaults = new Set(provider.defaultScopes);
|
|
288
|
+
return scopes.some((s) => !defaults.has(s));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Shows a warning note about non-default scopes, with optional
|
|
292
|
+
* provider-specific guidance, then prompts for confirmation.
|
|
293
|
+
* Returns false if the user cancels.
|
|
294
|
+
*/
|
|
295
|
+
async function confirmNonDefaultScopes(provider, scopes) {
|
|
296
|
+
if (!hasNonDefaultScopes(provider, scopes)) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
const defaults = new Set(provider.defaultScopes);
|
|
300
|
+
const nonDefault = scopes.filter((s) => !defaults.has(s));
|
|
301
|
+
const scopeList = nonDefault.map((s) => ` ${s}`).join('\n');
|
|
302
|
+
const guidance = provider.nonDefaultScopeWarning ??
|
|
303
|
+
'Ensure these scopes are configured with your OAuth provider\nbefore proceeding.';
|
|
304
|
+
p.note(`The following non-default scopes were selected:\n\n${scopeList}\n\n${guidance}`, 'Non-default scopes selected');
|
|
305
|
+
const confirmed = await p.confirm({
|
|
306
|
+
message: 'Continue with authorization?',
|
|
307
|
+
initialValue: true,
|
|
308
|
+
});
|
|
309
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
async function authorize(providerId, extraArgs) {
|
|
315
|
+
const provider = resolveProviderOrExit(providerId);
|
|
316
|
+
let credentials;
|
|
317
|
+
try {
|
|
318
|
+
credentials = loadClientCredentials(provider);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
322
|
+
process.stdout.write(`Invalid credentials for ${provider.displayName}: ${message}\n`);
|
|
323
|
+
process.stdout.write(`Re-import with: ironcurtain auth import ${provider.id} <credentials-file>\n`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
if (!credentials) {
|
|
327
|
+
process.stdout.write(`No credentials configured for ${provider.displayName}.\n` +
|
|
328
|
+
`Run 'ironcurtain auth import ${provider.id} <credentials-file>' first.\n`);
|
|
329
|
+
showSetupGuide(provider.id);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
const requestedScopes = parseScopesArg(extraArgs);
|
|
333
|
+
// Determine effective scopes (merge default + existing + requested for incremental consent)
|
|
334
|
+
let effectiveScopes;
|
|
335
|
+
let needsConfirmation = false;
|
|
336
|
+
if (requestedScopes) {
|
|
337
|
+
// --scopes flag: resolve short names and merge with existing
|
|
338
|
+
const resolved = provider.resolveShortScopes ? provider.resolveShortScopes(requestedScopes) : requestedScopes;
|
|
339
|
+
const existingToken = loadOAuthToken(provider.id);
|
|
340
|
+
const existingScopes = existingToken?.scopes ?? [];
|
|
341
|
+
// Always include provider defaults as baseline so --scopes only adds, never drops
|
|
342
|
+
const merged = [...new Set([...provider.defaultScopes, ...existingScopes, ...resolved])];
|
|
343
|
+
effectiveScopes = merged;
|
|
344
|
+
process.stdout.write(`\n ${provider.displayName} OAuth -- Incremental Consent\n\n`);
|
|
345
|
+
if (existingScopes.length > 0) {
|
|
346
|
+
process.stdout.write(' Existing scopes:\n');
|
|
347
|
+
for (const scope of existingScopes) {
|
|
348
|
+
process.stdout.write(` - ${scope}\n`);
|
|
349
|
+
}
|
|
350
|
+
process.stdout.write('\n Requesting additional scopes:\n');
|
|
351
|
+
for (const scope of resolved) {
|
|
352
|
+
if (!existingScopes.includes(scope)) {
|
|
353
|
+
process.stdout.write(` + ${scope}\n`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
process.stdout.write('\n');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (process.stdin.isTTY && provider.scopePicker) {
|
|
360
|
+
// Interactive TTY: show the scope picker
|
|
361
|
+
const existingToken = loadOAuthToken(provider.id);
|
|
362
|
+
const existingScopes = existingToken?.scopes ?? [];
|
|
363
|
+
const selected = await provider.scopePicker(existingScopes);
|
|
364
|
+
if (p.isCancel(selected)) {
|
|
365
|
+
p.cancel('Authorization cancelled.');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
effectiveScopes = selected;
|
|
369
|
+
needsConfirmation = true;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// Non-interactive: use defaults
|
|
373
|
+
process.stdout.write(`\n ${provider.displayName} OAuth\n\n`);
|
|
374
|
+
process.stdout.write(` Using credentials: ${truncate(credentials.clientId, 30)}\n`);
|
|
375
|
+
process.stdout.write(` Requesting scopes: ${provider.defaultScopes.join(', ')}\n\n`);
|
|
376
|
+
}
|
|
377
|
+
// Warn about non-default scopes and confirm (for interactive picker only)
|
|
378
|
+
if (effectiveScopes && needsConfirmation) {
|
|
379
|
+
const confirmed = await confirmNonDefaultScopes(provider, effectiveScopes);
|
|
380
|
+
if (!confirmed) {
|
|
381
|
+
p.cancel('Authorization cancelled.');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
process.stdout.write(' Opening browser for authorization...\n');
|
|
386
|
+
const result = await runOAuthFlow(provider, credentials, effectiveScopes);
|
|
387
|
+
saveOAuthToken(provider.id, result.token);
|
|
388
|
+
const tokenPath = getOAuthTokenPath(provider.id);
|
|
389
|
+
process.stdout.write(`\n Authorization successful!\n`);
|
|
390
|
+
process.stdout.write(` Token stored at ${tokenPath}\n`);
|
|
391
|
+
process.stdout.write(` Granted scopes: ${result.grantedScopes.join(', ')}\n\n`);
|
|
392
|
+
}
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// Entry point
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
export async function runAuthCommand(args) {
|
|
397
|
+
const subcommand = args[0];
|
|
398
|
+
// Show help when --help/-h is the first arg or no subcommand is given.
|
|
399
|
+
// Don't intercept --help after a subcommand — let subcommands handle their own help.
|
|
400
|
+
if (subcommand === '--help' || subcommand === '-h') {
|
|
401
|
+
printHelp(authSpec);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!subcommand || subcommand === 'status') {
|
|
405
|
+
showStatus();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (subcommand === 'import') {
|
|
409
|
+
importCredentials(args.slice(1));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (subcommand === 'revoke') {
|
|
413
|
+
const providerId = args[1];
|
|
414
|
+
if (!providerId) {
|
|
415
|
+
process.stdout.write('Usage: ironcurtain auth revoke <provider>\n');
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
await revokeToken(providerId);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// Remaining args after the provider name go to authorize for --scopes parsing
|
|
422
|
+
await authorize(subcommand, args.slice(1));
|
|
423
|
+
}
|
|
424
|
+
//# sourceMappingURL=auth-command.js.map
|