@s-gw/s-gw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +35 -0
- package/.mcp.json +16 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +197 -0
- package/TRADEMARKS.md +9 -0
- package/assets/icons/aws-ec2.png +0 -0
- package/assets/icons/lucide/bot.svg +8 -0
- package/assets/icons/lucide/monitor.svg +5 -0
- package/assets/icons/lucide/server.svg +6 -0
- package/assets/icons/lucide/terminal.svg +4 -0
- package/assets/icons/s-gw-128.png +0 -0
- package/assets/icons/s-gw-16.png +0 -0
- package/assets/icons/s-gw-180.png +0 -0
- package/assets/icons/s-gw-192.png +0 -0
- package/assets/icons/s-gw-32.png +0 -0
- package/assets/icons/s-gw-64.png +0 -0
- package/assets/icons/s-gw-menu-bar-template.png +0 -0
- package/dist/agent-context.d.ts +17 -0
- package/dist/agent-context.js +207 -0
- package/dist/agents.d.ts +64 -0
- package/dist/agents.js +763 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1385 -0
- package/dist/command-suggest.d.ts +3 -0
- package/dist/command-suggest.js +131 -0
- package/dist/console-server.d.ts +16 -0
- package/dist/console-server.js +978 -0
- package/dist/console-ui/assets/codex-DYTPdPxi.png +0 -0
- package/dist/console-ui/assets/cursor-CBrUTJD-.png +0 -0
- package/dist/console-ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/console-ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/console-ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/console-ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/console-ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/console-ui/assets/hermes-B8hNbJPm.png +0 -0
- package/dist/console-ui/assets/index-BxUf0Sye.js +96 -0
- package/dist/console-ui/assets/index-CmTiBR_w.css +2 -0
- package/dist/console-ui/assets/omnigent-Cxa4p2Mq.png +0 -0
- package/dist/console-ui/assets/openclaw-C5wL4ZVW.png +0 -0
- package/dist/console-ui/assets/opencode-D_wFATSC.png +0 -0
- package/dist/console-ui/assets/openhands-DnrlGgev.svg +9 -0
- package/dist/console-ui/assets/s-gw-64-ByMUGQ3K.png +0 -0
- package/dist/console-ui/assets/vscode-Bdtr9eyf.png +0 -0
- package/dist/console-ui/assets/zeptoclaw-DztQW8Sw.png +0 -0
- package/dist/console-ui/index.html +13 -0
- package/dist/crypto.d.ts +6 -0
- package/dist/crypto.js +53 -0
- package/dist/executor.d.ts +7 -0
- package/dist/executor.js +297 -0
- package/dist/gateway.d.ts +31 -0
- package/dist/gateway.js +114 -0
- package/dist/guard.d.ts +61 -0
- package/dist/guard.js +247 -0
- package/dist/install.d.ts +146 -0
- package/dist/install.js +629 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +119 -0
- package/dist/native/s-gw-core +0 -0
- package/dist/native/s-gw-keychain-helper +0 -0
- package/dist/onepassword.d.ts +48 -0
- package/dist/onepassword.js +412 -0
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +22 -0
- package/dist/s-gw Menu Bar.app/Contents/Info.plist +28 -0
- package/dist/s-gw Menu Bar.app/Contents/MacOS/s-gw-menu-bar-helper +0 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/AwsEc2.png +0 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-bot.svg +8 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-monitor.svg +5 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-server.svg +6 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-terminal.svg +4 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/MenuBarTemplate.png +0 -0
- package/dist/s-gw Menu Bar.app/Contents/_CodeSignature/CodeResources +194 -0
- package/dist/s-gw.app/Contents/Info.plist +28 -0
- package/dist/s-gw.app/Contents/MacOS/s-gw +0 -0
- package/dist/s-gw.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/s-gw.app/Contents/Resources/MenuBarTemplate.png +0 -0
- package/dist/s-gw.app/Contents/_CodeSignature/CodeResources +139 -0
- package/dist/scanner.d.ts +9 -0
- package/dist/scanner.js +437 -0
- package/dist/ssh.d.ts +31 -0
- package/dist/ssh.js +286 -0
- package/dist/store.d.ts +131 -0
- package/dist/store.js +1611 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.js +2 -0
- package/dist/unlock.d.ts +29 -0
- package/dist/unlock.js +274 -0
- package/dist/windows/VERSION.txt +1 -0
- package/dist/windows/s-gw-client.cmd +4 -0
- package/dist/windows/s-gw-client.ps1 +106 -0
- package/dist/windows/s-gw-credential.cmd +4 -0
- package/dist/windows/s-gw-credential.ps1 +167 -0
- package/dist/windows/s-gw-helper.cmd +4 -0
- package/dist/windows/s-gw-helper.ps1 +180 -0
- package/docs/README.md +23 -0
- package/docs/agents.md +160 -0
- package/docs/architecture.md +72 -0
- package/docs/deployment.md +447 -0
- package/docs/detection.md +44 -0
- package/docs/images/s-gw-overview.png +0 -0
- package/docs/integrations.md +195 -0
- package/docs/keychain.md +39 -0
- package/docs/onepassword.md +84 -0
- package/docs/quickstart.md +104 -0
- package/docs/threat-model.md +100 -0
- package/docs/ui/THIRD_PARTY_NOTICES.md +111 -0
- package/docs/ui/apple-touch-icon.png +0 -0
- package/docs/ui/favicon-32.png +0 -0
- package/docs/ui/local-console.html +4477 -0
- package/docs/ui/vendor/d3-sankey/d3-array.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-array.min.js +2 -0
- package/docs/ui/vendor/d3-sankey/d3-path.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-path.min.js +2 -0
- package/docs/ui/vendor/d3-sankey/d3-sankey.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-sankey.min.js +2 -0
- package/docs/ui/vendor/d3-sankey/d3-shape.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-shape.min.js +2 -0
- package/docs/ui/vendor/sankeymatic/LICENSE.txt +17 -0
- package/docs/ui/vendor/sankeymatic/sankey.js +897 -0
- package/package.json +117 -0
- package/skills/s-gw/SKILL.md +19 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { stdin } from "node:process";
|
|
6
|
+
import { getAgentCodeGuardPlan, listAgentProfiles, renderAgentMcpSnippet, resolveAgentProfile } from "./agents.js";
|
|
7
|
+
import { unknownCommandMessage } from "./command-suggest.js";
|
|
8
|
+
import { startConsoleServer } from "./console-server.js";
|
|
9
|
+
import { executeApprovedRequest } from "./executor.js";
|
|
10
|
+
import { buildEnvCommandAction, buildSshSessionAction, preferredLocalSecretBackend, scanLocalFile, scanTextToOnePassword } from "./gateway.js";
|
|
11
|
+
import { guardStatus, prepareGuardedRun, runGuardedAgent } from "./guard.js";
|
|
12
|
+
import { getPackageLayout, installConsoleLaunchAgent, installMenuBarLaunchAgent, launchAgentStatus, normalizeMenuBarCountMode, openMacApp, openMenuBarHelper, openWindowsClient, openWindowsHelper, packageHealth, startInstalledLaunchAgent, stopInstalledLaunchAgent, uninstallConsoleLaunchAgent, uninstallMenuBarLaunchAgent } from "./install.js";
|
|
13
|
+
import { listOnePasswordSecretReferences, onePasswordStatus, readOnePasswordReference } from "./onepassword.js";
|
|
14
|
+
import { SGW_SSH_SESSION_COMMAND, closeOwnedSshSession, defaultSshInjectEnv } from "./ssh.js";
|
|
15
|
+
import { SecretStore } from "./store.js";
|
|
16
|
+
import { deleteKeychainPassphrase, setKeychainPassphrase, unlockStatus } from "./unlock.js";
|
|
17
|
+
async function main() {
|
|
18
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
19
|
+
const store = new SecretStore();
|
|
20
|
+
const [first, second, third] = parsed.command;
|
|
21
|
+
if (!first || first === "help" || first === "--help") {
|
|
22
|
+
printHelp();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (first === "init") {
|
|
26
|
+
await store.init();
|
|
27
|
+
printJson({ ok: true, storePath: store.storePath });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (first === "setup") {
|
|
31
|
+
await handleSetupCommand(store, parsed.flags);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (first === "console") {
|
|
35
|
+
const host = getFlag(parsed.flags, "host") || "127.0.0.1";
|
|
36
|
+
const port = numericFlag(parsed.flags, "port", 8718);
|
|
37
|
+
let running;
|
|
38
|
+
try {
|
|
39
|
+
running = await startConsoleServer({ host, port, store });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (isAddressInUse(error)) {
|
|
43
|
+
// Very common first-run snag: `s-gw setup`/`s-gw start` already left a
|
|
44
|
+
// console daemon on this port, so the foreground `s-gw console` can't
|
|
45
|
+
// bind it. Don't dump a raw Node listen error — say what's going on.
|
|
46
|
+
throw new Error(`Port ${port} on ${host} is already in use — the s-gw console is probably already running at http://${host}:${port}/. ` +
|
|
47
|
+
`Open that URL, or run \`s-gw console --port <other>\` for a separate instance, or \`s-gw stop\` to stop the background console.`);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
if (!hasFlag(parsed.flags, "no-open")) {
|
|
52
|
+
openBrowser(running.url);
|
|
53
|
+
}
|
|
54
|
+
process.stdout.write(`s-gw console running at ${running.url}\n`);
|
|
55
|
+
process.stdout.write("Press Ctrl+C to stop.\n");
|
|
56
|
+
await waitForever();
|
|
57
|
+
await running.close();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (first === "doctor" || first === "status") {
|
|
61
|
+
printJson(packageHealth(numericFlag(parsed.flags, "port", 8718)));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (first === "start") {
|
|
65
|
+
await handleStartCommand(parsed.flags);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (first === "stop") {
|
|
69
|
+
await handleStopCommand();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (first === "service") {
|
|
73
|
+
await handleServiceCommand(second, parsed.flags);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (first === "menubar") {
|
|
77
|
+
await handleMenuBarCommand(second, parsed.flags);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (first === "helper") {
|
|
81
|
+
await handleMenuBarCommand(second, parsed.flags);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (first === "onepassword") {
|
|
85
|
+
await handleOnePasswordCommand(store, second, parsed.flags);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (first === "approval") {
|
|
89
|
+
await handleApprovalCommand(store, second, third, parsed.flags);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (first === "ssh") {
|
|
93
|
+
await handleSshCommand(store, second, third, parsed.flags);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (first === "aws") {
|
|
97
|
+
await handleAwsCommand(store, second, parsed.flags, parsed.command.slice(2));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (first === "app") {
|
|
101
|
+
await handleAppCommand(second, parsed.flags);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (first === "guard") {
|
|
105
|
+
await handleGuardCommand(store, second, third, parsed.flags);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (first === "run") {
|
|
109
|
+
await handleGuardRun(store, second, parsed.flags);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (first === "unlock" && second === "status") {
|
|
113
|
+
printJson(unlockStatus());
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (first === "unlock" && second === "keychain" && third === "set") {
|
|
117
|
+
const passphrase = await readStdinValue(parsed.flags, "value-stdin", "unlock passphrase");
|
|
118
|
+
setKeychainPassphrase(passphrase);
|
|
119
|
+
printJson({
|
|
120
|
+
ok: true,
|
|
121
|
+
keychain: unlockStatus().keychain
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (first === "unlock" && second === "keychain" && third === "delete") {
|
|
126
|
+
printJson({
|
|
127
|
+
deleted: deleteKeychainPassphrase(),
|
|
128
|
+
keychain: unlockStatus().keychain
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (first === "secret" && second === "add") {
|
|
133
|
+
const value = await readStdinValue(parsed.flags, "value-stdin", "secret");
|
|
134
|
+
const record = await store.addSecret({
|
|
135
|
+
name: requireFlag(parsed.flags, "name"),
|
|
136
|
+
type: secretType(getFlag(parsed.flags, "type") || "unknown"),
|
|
137
|
+
value,
|
|
138
|
+
policy: {
|
|
139
|
+
injectEnv: getFlag(parsed.flags, "inject-env"),
|
|
140
|
+
allowedCommands: getFlagList(parsed.flags, "allow-command"),
|
|
141
|
+
maxOutputBytes: numericFlag(parsed.flags, "max-output-bytes", 16_384)
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
printJson({
|
|
145
|
+
handle: record.handle,
|
|
146
|
+
name: record.name,
|
|
147
|
+
type: record.type,
|
|
148
|
+
policy: record.policy
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (first === "secret" && (second === "add-keychain" || second === "add-kc")) {
|
|
153
|
+
const value = await readStdinValue(parsed.flags, "value-stdin", "secret");
|
|
154
|
+
const record = await store.addKeychainSecret({
|
|
155
|
+
name: requireFlag(parsed.flags, "name"),
|
|
156
|
+
type: secretType(getFlag(parsed.flags, "type") || "unknown"),
|
|
157
|
+
value,
|
|
158
|
+
service: getFlag(parsed.flags, "service"),
|
|
159
|
+
source: getFlag(parsed.flags, "source") || "macos-keychain",
|
|
160
|
+
policy: {
|
|
161
|
+
injectEnv: getFlag(parsed.flags, "inject-env"),
|
|
162
|
+
allowedCommands: getFlagList(parsed.flags, "allow-command"),
|
|
163
|
+
maxOutputBytes: numericFlag(parsed.flags, "max-output-bytes", 16_384)
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
printJson({
|
|
167
|
+
handle: record.handle,
|
|
168
|
+
name: record.name,
|
|
169
|
+
type: record.type,
|
|
170
|
+
provider: record.provider,
|
|
171
|
+
backend: record.backend,
|
|
172
|
+
policy: record.policy
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (first === "secret" && (second === "add-1password" || second === "add-op")) {
|
|
177
|
+
const reference = requireFlag(parsed.flags, "ref");
|
|
178
|
+
if (hasFlag(parsed.flags, "verify")) {
|
|
179
|
+
await readOnePasswordReference(reference);
|
|
180
|
+
}
|
|
181
|
+
const record = await store.addOnePasswordReference({
|
|
182
|
+
name: requireFlag(parsed.flags, "name"),
|
|
183
|
+
type: secretType(getFlag(parsed.flags, "type") || "unknown"),
|
|
184
|
+
reference,
|
|
185
|
+
policy: {
|
|
186
|
+
injectEnv: getFlag(parsed.flags, "inject-env"),
|
|
187
|
+
allowedCommands: getFlagList(parsed.flags, "allow-command"),
|
|
188
|
+
maxOutputBytes: numericFlag(parsed.flags, "max-output-bytes", 16_384)
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
printJson({
|
|
192
|
+
handle: record.handle,
|
|
193
|
+
name: record.name,
|
|
194
|
+
type: record.type,
|
|
195
|
+
provider: record.provider,
|
|
196
|
+
backend: record.backend,
|
|
197
|
+
verified: hasFlag(parsed.flags, "verify"),
|
|
198
|
+
policy: record.policy
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (first === "secret" && second === "list") {
|
|
203
|
+
const handles = await store.listHandles();
|
|
204
|
+
// An empty list with no unlock source is ambiguous: it reads like "no secrets yet"
|
|
205
|
+
// when really the ledger can't be unlocked at all. Nudge to stderr so the stdout JSON
|
|
206
|
+
// stays clean for scripts.
|
|
207
|
+
if (handles.length === 0 && unlockStatus().activeSource === "none") {
|
|
208
|
+
process.stderr.write("s-gw: no secrets, and no unlock material is configured. Run `s-gw setup` before enrolling secrets.\n");
|
|
209
|
+
}
|
|
210
|
+
printJson(handles);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (first === "secret" && (second === "delete" || second === "remove")) {
|
|
214
|
+
const handle = third || requireFlag(parsed.flags, "handle");
|
|
215
|
+
printJson(await store.deleteSecret(handle));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (first === "secret" && second === "allow-command") {
|
|
219
|
+
const handle = third || requireFlag(parsed.flags, "handle");
|
|
220
|
+
const command = getFlag(parsed.flags, "command") || SGW_SSH_SESSION_COMMAND;
|
|
221
|
+
printJson(await store.allowCommand(handle, command));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (first === "scan-file") {
|
|
225
|
+
const target = second;
|
|
226
|
+
if (!target) {
|
|
227
|
+
throw new Error("scan-file requires a path.");
|
|
228
|
+
}
|
|
229
|
+
const persist = !hasFlag(parsed.flags, "preview");
|
|
230
|
+
printJson(await scanLocalFile(store, target, { persist, backend: secretBackendFlag(parsed.flags) }));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (first === "agent" && second === "list") {
|
|
234
|
+
printJson(listAgentProfiles());
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (first === "agent" && second === "show") {
|
|
238
|
+
if (!third) {
|
|
239
|
+
throw new Error("agent show requires an agent name.");
|
|
240
|
+
}
|
|
241
|
+
const profile = resolveAgentProfile(third);
|
|
242
|
+
printJson({
|
|
243
|
+
...profile,
|
|
244
|
+
mcpSnippet: profile.mcp.supported ? renderAgentMcpSnippet(profile.id, mcpSnippetOptions(parsed.flags)) : null,
|
|
245
|
+
codeGuardPlan: getAgentCodeGuardPlan(profile.id)
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (first === "agent" && second === "codeguard-plan") {
|
|
250
|
+
if (!third) {
|
|
251
|
+
throw new Error("agent codeguard-plan requires an agent name.");
|
|
252
|
+
}
|
|
253
|
+
printJson(getAgentCodeGuardPlan(third));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (first === "agent" && second === "mcp-snippet") {
|
|
257
|
+
if (!third) {
|
|
258
|
+
throw new Error("agent mcp-snippet requires an agent name.");
|
|
259
|
+
}
|
|
260
|
+
process.stdout.write(`${renderAgentMcpSnippet(third, mcpSnippetOptions(parsed.flags))}\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (first === "requests") {
|
|
264
|
+
if (hasFlag(parsed.flags, "recover")) {
|
|
265
|
+
// --recover (clear all stranded) or --recover REQ / `requests recover REQ` (one).
|
|
266
|
+
const target = second || getFlag(parsed.flags, "recover");
|
|
267
|
+
const recovered = await store.forceRecoverExecutions(typeof target === "string" ? target : undefined);
|
|
268
|
+
printJson({ recoveredCount: recovered.length, recovered });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (second === "cleanup" || hasFlag(parsed.flags, "cleanup")) {
|
|
272
|
+
printJson(await store.cleanupRequests({
|
|
273
|
+
pendingOlderThanMs: numericFlag(parsed.flags, "pending-ttl-ms", 24 * 60 * 60 * 1000),
|
|
274
|
+
approvedOlderThanMs: numericFlag(parsed.flags, "approved-ttl-ms", 60 * 60 * 1000),
|
|
275
|
+
duplicatePending: !hasFlag(parsed.flags, "no-dedupe")
|
|
276
|
+
}));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const state = getFlag(parsed.flags, "state");
|
|
280
|
+
printJson(await store.listRequests({
|
|
281
|
+
state,
|
|
282
|
+
active: hasFlag(parsed.flags, "active"),
|
|
283
|
+
limit: hasFlag(parsed.flags, "all") ? undefined : numericFlag(parsed.flags, "limit", 100)
|
|
284
|
+
}));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (first === "approve") {
|
|
288
|
+
if (!second) {
|
|
289
|
+
throw new Error("approve requires a request id.");
|
|
290
|
+
}
|
|
291
|
+
const mode = getFlag(parsed.flags, "mode");
|
|
292
|
+
const duration = getFlag(parsed.flags, "duration") || getFlag(parsed.flags, "duration-ms");
|
|
293
|
+
const agentScope = getFlag(parsed.flags, "agent-scope");
|
|
294
|
+
printJson(await store.approveRequest(second, {
|
|
295
|
+
mode: mode ? approvalMode(mode) : undefined,
|
|
296
|
+
durationMs: duration ? parseDurationMs(duration) : undefined,
|
|
297
|
+
agentScope: agentScope ? approvalAgentScope(agentScope) : undefined
|
|
298
|
+
}));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (first === "deny") {
|
|
302
|
+
if (!second) {
|
|
303
|
+
throw new Error("deny requires a request id.");
|
|
304
|
+
}
|
|
305
|
+
printJson(await store.denyRequest(second));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (first === "execute") {
|
|
309
|
+
if (!second) {
|
|
310
|
+
throw new Error("execute requires a request id.");
|
|
311
|
+
}
|
|
312
|
+
printJson(await executeApprovedRequest(store, second));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (first === "execute-next") {
|
|
316
|
+
const request = await findNextApprovedRequest(store, parsed.flags);
|
|
317
|
+
printJson({
|
|
318
|
+
requestId: request.id,
|
|
319
|
+
summary: await executeApprovedRequest(store, request.id)
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (first === "request" && second === "env-command") {
|
|
324
|
+
const handle = third || requireFlag(parsed.flags, "handle");
|
|
325
|
+
const action = buildEnvCommandAction({
|
|
326
|
+
command: requireFlag(parsed.flags, "command"),
|
|
327
|
+
args: commandArgsFromFlags(parsed.flags),
|
|
328
|
+
injectEnv: requireFlag(parsed.flags, "inject-env"),
|
|
329
|
+
env: envBindingsFromFlags(parsed.flags),
|
|
330
|
+
workingDir: getFlag(parsed.flags, "cwd"),
|
|
331
|
+
timeoutMs: numericFlag(parsed.flags, "timeout-ms", 30_000)
|
|
332
|
+
});
|
|
333
|
+
printJson(await store.createRequest(handle, action, getFlag(parsed.flags, "reason") || "Local CLI request"));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (first === "store" && (second === "backups" || second === "backup")) {
|
|
337
|
+
printJson(await store.listStoreBackups());
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
throw new Error(unknownCommandMessage(parsed.command));
|
|
341
|
+
}
|
|
342
|
+
function parseArgs(args) {
|
|
343
|
+
const command = [];
|
|
344
|
+
const flags = {};
|
|
345
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
346
|
+
const item = args[i];
|
|
347
|
+
if (item === "--") {
|
|
348
|
+
flags["--"] = args.slice(i + 1);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (!item.startsWith("--")) {
|
|
352
|
+
command.push(item);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const equals = item.indexOf("=");
|
|
356
|
+
if (equals > 2) {
|
|
357
|
+
addFlag(flags, item.slice(2, equals), item.slice(equals + 1));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const key = item.slice(2);
|
|
361
|
+
const next = args[i + 1];
|
|
362
|
+
if (next !== undefined && flagTakesValue(key) && (!next.startsWith("--") || key === "arg")) {
|
|
363
|
+
addFlag(flags, key, next);
|
|
364
|
+
i += 1;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (!next || next.startsWith("--")) {
|
|
368
|
+
addFlag(flags, key, true);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
addFlag(flags, key, next);
|
|
372
|
+
i += 1;
|
|
373
|
+
}
|
|
374
|
+
return { command, flags };
|
|
375
|
+
}
|
|
376
|
+
const valueFlags = new Set([
|
|
377
|
+
"access-handle",
|
|
378
|
+
"agent",
|
|
379
|
+
"allow-command",
|
|
380
|
+
"action-kind",
|
|
381
|
+
"approved-ttl-ms",
|
|
382
|
+
"arg",
|
|
383
|
+
"args-json",
|
|
384
|
+
"backend",
|
|
385
|
+
"command",
|
|
386
|
+
"console-url",
|
|
387
|
+
"count",
|
|
388
|
+
"cwd",
|
|
389
|
+
"decision",
|
|
390
|
+
"duration",
|
|
391
|
+
"duration-ms",
|
|
392
|
+
"env",
|
|
393
|
+
"expires-at",
|
|
394
|
+
"handle",
|
|
395
|
+
"id",
|
|
396
|
+
"inject-env",
|
|
397
|
+
"kind",
|
|
398
|
+
"limit",
|
|
399
|
+
"max-output-bytes",
|
|
400
|
+
"menubar-count",
|
|
401
|
+
"min-severity",
|
|
402
|
+
"mode",
|
|
403
|
+
"name",
|
|
404
|
+
"pending-ttl-ms",
|
|
405
|
+
"port",
|
|
406
|
+
"priority",
|
|
407
|
+
"provider",
|
|
408
|
+
"reason",
|
|
409
|
+
"recover",
|
|
410
|
+
"ref",
|
|
411
|
+
"secret-handle",
|
|
412
|
+
"service",
|
|
413
|
+
"server-name",
|
|
414
|
+
"source",
|
|
415
|
+
"ssh-port",
|
|
416
|
+
"ssh-target",
|
|
417
|
+
"state",
|
|
418
|
+
"target",
|
|
419
|
+
"timeout-ms",
|
|
420
|
+
"type",
|
|
421
|
+
"vault",
|
|
422
|
+
"with-env",
|
|
423
|
+
"working-dir",
|
|
424
|
+
"wrapper"
|
|
425
|
+
]);
|
|
426
|
+
function flagTakesValue(key) {
|
|
427
|
+
return valueFlags.has(key);
|
|
428
|
+
}
|
|
429
|
+
function addFlag(flags, key, value) {
|
|
430
|
+
const existing = flags[key];
|
|
431
|
+
if (existing === undefined) {
|
|
432
|
+
flags[key] = value;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (Array.isArray(existing)) {
|
|
436
|
+
existing.push(String(value));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
flags[key] = [String(existing), String(value)];
|
|
440
|
+
}
|
|
441
|
+
async function readStdinValue(flags, flagName, label) {
|
|
442
|
+
if (!hasFlag(flags, flagName)) {
|
|
443
|
+
throw new Error(`Use --${flagName} so the ${label} never appears in shell history or chat.`);
|
|
444
|
+
}
|
|
445
|
+
const chunks = [];
|
|
446
|
+
for await (const chunk of stdin) {
|
|
447
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
448
|
+
}
|
|
449
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
450
|
+
}
|
|
451
|
+
function getFlag(flags, key) {
|
|
452
|
+
const value = flags[key];
|
|
453
|
+
if (Array.isArray(value)) {
|
|
454
|
+
return value.at(-1);
|
|
455
|
+
}
|
|
456
|
+
if (typeof value === "string") {
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
function getFlagList(flags, key) {
|
|
462
|
+
const value = flags[key];
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
if (typeof value === "string") {
|
|
467
|
+
return [value];
|
|
468
|
+
}
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
function commandArgsFromFlags(flags) {
|
|
472
|
+
const passthrough = getFlagList(flags, "--");
|
|
473
|
+
if (passthrough.length > 0) {
|
|
474
|
+
return passthrough;
|
|
475
|
+
}
|
|
476
|
+
const jsonArgs = getFlag(flags, "args-json");
|
|
477
|
+
if (jsonArgs) {
|
|
478
|
+
const parsed = JSON.parse(jsonArgs);
|
|
479
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) {
|
|
480
|
+
throw new Error("--args-json must be a JSON array of strings.");
|
|
481
|
+
}
|
|
482
|
+
return parsed;
|
|
483
|
+
}
|
|
484
|
+
return getFlagList(flags, "arg");
|
|
485
|
+
}
|
|
486
|
+
function envBindingsFromFlags(flags) {
|
|
487
|
+
const values = getFlagList(flags, "with-env");
|
|
488
|
+
const out = [];
|
|
489
|
+
for (const value of values) {
|
|
490
|
+
const index = value.indexOf("=");
|
|
491
|
+
if (index <= 0 || index === value.length - 1) {
|
|
492
|
+
throw new Error("--with-env must look like ENV=HANDLE.");
|
|
493
|
+
}
|
|
494
|
+
out.push({
|
|
495
|
+
injectEnv: value.slice(0, index),
|
|
496
|
+
handle: value.slice(index + 1)
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
async function findNextApprovedRequest(store, flags) {
|
|
502
|
+
const handle = getFlag(flags, "handle");
|
|
503
|
+
const kind = getFlag(flags, "kind");
|
|
504
|
+
const command = getFlag(flags, "command");
|
|
505
|
+
let requests = await store.listRequests({ state: "approved", limit: 1000 });
|
|
506
|
+
if (handle) {
|
|
507
|
+
requests = requests.filter((request) => request.handle === handle);
|
|
508
|
+
}
|
|
509
|
+
if (kind) {
|
|
510
|
+
requests = requests.filter((request) => request.action.kind === kind);
|
|
511
|
+
}
|
|
512
|
+
if (command) {
|
|
513
|
+
requests = requests.filter((request) => request.action.command === command);
|
|
514
|
+
}
|
|
515
|
+
if (requests.length === 0) {
|
|
516
|
+
throw new Error("No approved request matches execute-next filters.");
|
|
517
|
+
}
|
|
518
|
+
if (requests.length > 1 && !handle && !command) {
|
|
519
|
+
throw new Error("Multiple approved requests match. Add --handle or --command before using execute-next.");
|
|
520
|
+
}
|
|
521
|
+
return requests[0];
|
|
522
|
+
}
|
|
523
|
+
function requireFlag(flags, key) {
|
|
524
|
+
const value = getFlag(flags, key);
|
|
525
|
+
if (!value) {
|
|
526
|
+
throw new Error(`Missing --${key}.`);
|
|
527
|
+
}
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
function hasFlag(flags, key) {
|
|
531
|
+
return flags[key] !== undefined;
|
|
532
|
+
}
|
|
533
|
+
function numericFlag(flags, key, fallback) {
|
|
534
|
+
const raw = getFlag(flags, key);
|
|
535
|
+
if (!raw) {
|
|
536
|
+
return fallback;
|
|
537
|
+
}
|
|
538
|
+
const parsed = Number(raw);
|
|
539
|
+
if (!Number.isFinite(parsed)) {
|
|
540
|
+
throw new Error(`--${key} must be a number.`);
|
|
541
|
+
}
|
|
542
|
+
return parsed;
|
|
543
|
+
}
|
|
544
|
+
function optionalNumericFlag(flags, key) {
|
|
545
|
+
const raw = getFlag(flags, key);
|
|
546
|
+
if (!raw) {
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
const parsed = Number(raw);
|
|
550
|
+
if (!Number.isFinite(parsed)) {
|
|
551
|
+
throw new Error(`--${key} must be a number.`);
|
|
552
|
+
}
|
|
553
|
+
return parsed;
|
|
554
|
+
}
|
|
555
|
+
function secretBackendFlag(flags) {
|
|
556
|
+
const raw = getFlag(flags, "backend");
|
|
557
|
+
if (!raw) {
|
|
558
|
+
return preferredLocalSecretBackend();
|
|
559
|
+
}
|
|
560
|
+
const normalized = raw.trim().toLowerCase();
|
|
561
|
+
if (normalized === "local" || normalized === "keychain") {
|
|
562
|
+
return normalized;
|
|
563
|
+
}
|
|
564
|
+
throw new Error("--backend must be either local or keychain.");
|
|
565
|
+
}
|
|
566
|
+
function secretType(input) {
|
|
567
|
+
const allowed = [
|
|
568
|
+
"api-token",
|
|
569
|
+
"ssh-key",
|
|
570
|
+
"private-key",
|
|
571
|
+
"password",
|
|
572
|
+
"credential",
|
|
573
|
+
"access-key",
|
|
574
|
+
"unknown"
|
|
575
|
+
];
|
|
576
|
+
return allowed.includes(input) ? input : "unknown";
|
|
577
|
+
}
|
|
578
|
+
function approvalMode(input) {
|
|
579
|
+
const normalized = input.trim().toLowerCase();
|
|
580
|
+
switch (normalized) {
|
|
581
|
+
case "per-transaction":
|
|
582
|
+
case "transaction":
|
|
583
|
+
case "per":
|
|
584
|
+
return "per-transaction";
|
|
585
|
+
case "timed-session":
|
|
586
|
+
case "timed":
|
|
587
|
+
case "time":
|
|
588
|
+
return "timed-session";
|
|
589
|
+
case "login-session":
|
|
590
|
+
case "login":
|
|
591
|
+
case "session":
|
|
592
|
+
return "login-session";
|
|
593
|
+
case "always":
|
|
594
|
+
case "unlimited":
|
|
595
|
+
case "forever":
|
|
596
|
+
return "always";
|
|
597
|
+
default:
|
|
598
|
+
throw new Error("--mode must be per-transaction, timed-session, login-session, or always.");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function approvalAgentScope(input) {
|
|
602
|
+
const normalized = input.trim().toLowerCase();
|
|
603
|
+
switch (normalized) {
|
|
604
|
+
case "same-agent":
|
|
605
|
+
case "agent":
|
|
606
|
+
case "this-agent":
|
|
607
|
+
case "same":
|
|
608
|
+
return "same-agent";
|
|
609
|
+
case "any-agent":
|
|
610
|
+
case "all-agents":
|
|
611
|
+
case "all":
|
|
612
|
+
case "any":
|
|
613
|
+
return "any-agent";
|
|
614
|
+
default:
|
|
615
|
+
throw new Error("--agent-scope must be same-agent or any-agent.");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function approvalPolicyDecision(input) {
|
|
619
|
+
const normalized = input.trim().toLowerCase();
|
|
620
|
+
if (normalized === "allow" || normalized === "ask" || normalized === "deny") {
|
|
621
|
+
return normalized;
|
|
622
|
+
}
|
|
623
|
+
throw new Error("--decision must be allow, ask, or deny.");
|
|
624
|
+
}
|
|
625
|
+
function approvalPolicyActionKind(input) {
|
|
626
|
+
const normalized = input.trim().toLowerCase();
|
|
627
|
+
if (normalized === "env-command" || normalized === "env_command" || normalized === "command") {
|
|
628
|
+
return "env_command";
|
|
629
|
+
}
|
|
630
|
+
if (normalized === "ssh-session" || normalized === "ssh_session" || normalized === "ssh") {
|
|
631
|
+
return "ssh_session";
|
|
632
|
+
}
|
|
633
|
+
throw new Error("--action-kind must be env_command or ssh_session.");
|
|
634
|
+
}
|
|
635
|
+
function optionalSecretSeverity(input) {
|
|
636
|
+
if (!input) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
const normalized = input.trim().toLowerCase();
|
|
640
|
+
if (normalized === "low" || normalized === "medium" || normalized === "high" || normalized === "critical") {
|
|
641
|
+
return normalized;
|
|
642
|
+
}
|
|
643
|
+
throw new Error("--min-severity must be low, medium, high, or critical.");
|
|
644
|
+
}
|
|
645
|
+
function parseDurationMs(input) {
|
|
646
|
+
const trimmed = input.trim().toLowerCase();
|
|
647
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*(ms|msec|millisecond|milliseconds|s|sec|second|seconds|m|min|minute|minutes|h|hr|hour|hours|d|day|days)?$/);
|
|
648
|
+
if (!match) {
|
|
649
|
+
throw new Error("--duration must look like 15m, 2h, 1d, or a millisecond number.");
|
|
650
|
+
}
|
|
651
|
+
const value = Number(match[1]);
|
|
652
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
653
|
+
throw new Error("--duration must be a positive number.");
|
|
654
|
+
}
|
|
655
|
+
const unit = match[2] || "ms";
|
|
656
|
+
if (unit === "ms" || unit === "msec" || unit.startsWith("millisecond")) {
|
|
657
|
+
return Math.floor(value);
|
|
658
|
+
}
|
|
659
|
+
if (unit === "s" || unit === "sec" || unit.startsWith("second")) {
|
|
660
|
+
return Math.floor(value * 1000);
|
|
661
|
+
}
|
|
662
|
+
if (unit === "m" || unit === "min" || unit.startsWith("minute")) {
|
|
663
|
+
return Math.floor(value * 60 * 1000);
|
|
664
|
+
}
|
|
665
|
+
if (unit === "h" || unit === "hr" || unit.startsWith("hour")) {
|
|
666
|
+
return Math.floor(value * 60 * 60 * 1000);
|
|
667
|
+
}
|
|
668
|
+
if (unit === "d" || unit.startsWith("day")) {
|
|
669
|
+
return Math.floor(value * 24 * 60 * 60 * 1000);
|
|
670
|
+
}
|
|
671
|
+
throw new Error(`Unsupported duration unit: ${unit}`);
|
|
672
|
+
}
|
|
673
|
+
function mcpSnippetOptions(flags) {
|
|
674
|
+
return {
|
|
675
|
+
serverName: getFlag(flags, "server-name"),
|
|
676
|
+
command: getFlag(flags, "command"),
|
|
677
|
+
args: getFlagList(flags, "arg"),
|
|
678
|
+
env: parseEnvFlags(getFlagList(flags, "env"))
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function parseEnvFlags(values) {
|
|
682
|
+
const env = {};
|
|
683
|
+
for (const value of values) {
|
|
684
|
+
const idx = value.indexOf("=");
|
|
685
|
+
if (idx <= 0) {
|
|
686
|
+
throw new Error(`--env values must be KEY=VALUE. Got: ${value}`);
|
|
687
|
+
}
|
|
688
|
+
const key = value.slice(0, idx).trim();
|
|
689
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
690
|
+
throw new Error(`Invalid env key for --env: ${key}`);
|
|
691
|
+
}
|
|
692
|
+
env[key] = value.slice(idx + 1);
|
|
693
|
+
}
|
|
694
|
+
return env;
|
|
695
|
+
}
|
|
696
|
+
async function handleSetupCommand(store, flags) {
|
|
697
|
+
const port = numericFlag(flags, "port", 8718);
|
|
698
|
+
const beforeUnlock = unlockStatus();
|
|
699
|
+
let unlockAction = beforeUnlock.activeSource === "none" ? "not-configured" : `existing-${beforeUnlock.activeSource}`;
|
|
700
|
+
if (beforeUnlock.activeSource === "none") {
|
|
701
|
+
if (process.platform !== "darwin" && process.platform !== "win32") {
|
|
702
|
+
throw new Error("s-gw setup currently needs a local OS credential store. On Linux, set SGW_MASTER_PASSPHRASE and run s-gw init.");
|
|
703
|
+
}
|
|
704
|
+
const passphrase = hasFlag(flags, "passphrase-stdin")
|
|
705
|
+
? await readStdinValue(flags, "passphrase-stdin", "unlock passphrase")
|
|
706
|
+
: randomBytes(32).toString("base64url");
|
|
707
|
+
setKeychainPassphrase(passphrase);
|
|
708
|
+
unlockAction = hasFlag(flags, "passphrase-stdin") ? "stored-keychain-passphrase" : "generated-keychain-passphrase";
|
|
709
|
+
}
|
|
710
|
+
await store.init();
|
|
711
|
+
const consoleUrl = `http://127.0.0.1:${port}/`;
|
|
712
|
+
let service = launchAgentStatus("console");
|
|
713
|
+
let menuBar = launchAgentStatus("menubar");
|
|
714
|
+
let windowsHelper;
|
|
715
|
+
if (process.platform === "darwin" && !hasFlag(flags, "no-service")) {
|
|
716
|
+
service = await installConsoleLaunchAgent({ port, start: true });
|
|
717
|
+
}
|
|
718
|
+
if (process.platform === "darwin" && !hasFlag(flags, "no-menubar")) {
|
|
719
|
+
menuBar = await installMenuBarLaunchAgent({
|
|
720
|
+
port,
|
|
721
|
+
start: true,
|
|
722
|
+
notify: !hasFlag(flags, "no-notify"),
|
|
723
|
+
countMode: normalizeMenuBarCountMode(getFlag(flags, "menubar-count"))
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
else if (process.platform === "win32" && !hasFlag(flags, "no-menubar")) {
|
|
727
|
+
windowsHelper = openWindowsHelper({ port, consoleUrl });
|
|
728
|
+
}
|
|
729
|
+
const opened = shouldOpenUi(flags) ? openPreferredUi(port, consoleUrl) : undefined;
|
|
730
|
+
printJson({
|
|
731
|
+
ok: true,
|
|
732
|
+
unlock: unlockAction,
|
|
733
|
+
storePath: store.storePath,
|
|
734
|
+
consoleUrl,
|
|
735
|
+
opened,
|
|
736
|
+
service,
|
|
737
|
+
menuBar,
|
|
738
|
+
windowsHelper,
|
|
739
|
+
nextSteps: [
|
|
740
|
+
"Open the native app with `s-gw app open`.",
|
|
741
|
+
"Run `s-gw agent mcp-snippet codex` or another known agent profile.",
|
|
742
|
+
"Enroll secrets locally with `s-gw secret add-keychain --value-stdin` or scan files with `s-gw scan-file PATH`."
|
|
743
|
+
]
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
async function handleStartCommand(flags) {
|
|
747
|
+
const port = numericFlag(flags, "port", 8718);
|
|
748
|
+
const consoleUrl = `http://127.0.0.1:${port}/`;
|
|
749
|
+
if (process.platform === "win32") {
|
|
750
|
+
const helper = openWindowsHelper({ port, consoleUrl });
|
|
751
|
+
const opened = shouldOpenUi(flags) ? openPreferredUi(port, consoleUrl) : undefined;
|
|
752
|
+
printJson({ ok: true, consoleUrl, opened, helper });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const service = launchAgentStatus("console").installed
|
|
756
|
+
? startInstalledLaunchAgent("console")
|
|
757
|
+
: await installConsoleLaunchAgent({ port, start: true });
|
|
758
|
+
const menuBar = launchAgentStatus("menubar").installed
|
|
759
|
+
? startInstalledLaunchAgent("menubar")
|
|
760
|
+
: await installMenuBarLaunchAgent({ port, start: true });
|
|
761
|
+
const opened = shouldOpenUi(flags) ? openPreferredUi(port, consoleUrl) : undefined;
|
|
762
|
+
printJson({ ok: true, consoleUrl, opened, service, menuBar });
|
|
763
|
+
}
|
|
764
|
+
async function handleStopCommand() {
|
|
765
|
+
const service = launchAgentStatus("console").installed ? stopInstalledLaunchAgent("console") : launchAgentStatus("console");
|
|
766
|
+
const menuBar = launchAgentStatus("menubar").installed ? stopInstalledLaunchAgent("menubar") : launchAgentStatus("menubar");
|
|
767
|
+
printJson({ ok: true, service, menuBar });
|
|
768
|
+
}
|
|
769
|
+
async function handleServiceCommand(action, flags) {
|
|
770
|
+
if (action === "install") {
|
|
771
|
+
printJson(await installConsoleLaunchAgent({
|
|
772
|
+
port: numericFlag(flags, "port", 8718),
|
|
773
|
+
start: hasFlag(flags, "start")
|
|
774
|
+
}));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (action === "start") {
|
|
778
|
+
printJson(startInstalledLaunchAgent("console"));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (action === "stop") {
|
|
782
|
+
printJson(stopInstalledLaunchAgent("console"));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (action === "status") {
|
|
786
|
+
printJson(launchAgentStatus("console"));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (action === "uninstall") {
|
|
790
|
+
printJson(await uninstallConsoleLaunchAgent());
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
throw new Error("service requires install, start, stop, status, or uninstall.");
|
|
794
|
+
}
|
|
795
|
+
async function handleMenuBarCommand(action, flags) {
|
|
796
|
+
if (action === "app-path") {
|
|
797
|
+
const layout = getPackageLayout();
|
|
798
|
+
process.stdout.write(`${process.platform === "win32" ? layout.windowsHelperLauncherPath : layout.menuBarAppPath}\n`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (action === "open") {
|
|
802
|
+
if (process.platform === "win32") {
|
|
803
|
+
printJson(openWindowsHelper({
|
|
804
|
+
consoleUrl: getFlag(flags, "console-url"),
|
|
805
|
+
port: numericFlag(flags, "port", 8718)
|
|
806
|
+
}));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
printJson(openMenuBarHelper({
|
|
810
|
+
consoleUrl: getFlag(flags, "console-url"),
|
|
811
|
+
port: numericFlag(flags, "port", 8718),
|
|
812
|
+
show: hasFlag(flags, "show"),
|
|
813
|
+
notify: !hasFlag(flags, "no-notify"),
|
|
814
|
+
countMode: normalizeMenuBarCountMode(getFlag(flags, "count"))
|
|
815
|
+
}));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (action === "install") {
|
|
819
|
+
printJson(await installMenuBarLaunchAgent({
|
|
820
|
+
consoleUrl: getFlag(flags, "console-url"),
|
|
821
|
+
port: numericFlag(flags, "port", 8718),
|
|
822
|
+
start: hasFlag(flags, "start"),
|
|
823
|
+
notify: !hasFlag(flags, "no-notify"),
|
|
824
|
+
countMode: normalizeMenuBarCountMode(getFlag(flags, "count"))
|
|
825
|
+
}));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (action === "start") {
|
|
829
|
+
printJson(startInstalledLaunchAgent("menubar"));
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (action === "stop") {
|
|
833
|
+
printJson(stopInstalledLaunchAgent("menubar"));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (action === "status") {
|
|
837
|
+
printJson(launchAgentStatus("menubar"));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (action === "uninstall") {
|
|
841
|
+
printJson(await uninstallMenuBarLaunchAgent());
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
throw new Error("menubar requires app-path, open, install, start, stop, status, or uninstall.");
|
|
845
|
+
}
|
|
846
|
+
async function handleOnePasswordCommand(store, action, flags) {
|
|
847
|
+
if (!action || action === "status") {
|
|
848
|
+
printJson(onePasswordStatus());
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (action === "import") {
|
|
852
|
+
const vault = getFlag(flags, "vault") || "Dev";
|
|
853
|
+
const candidates = await listOnePasswordSecretReferences(vault);
|
|
854
|
+
if (hasFlag(flags, "dry-run")) {
|
|
855
|
+
printJson({
|
|
856
|
+
vault,
|
|
857
|
+
candidates: candidates.map((candidate) => ({
|
|
858
|
+
name: onePasswordHandleName(candidate.itemTitle, candidate.fieldLabel),
|
|
859
|
+
type: candidate.secretType,
|
|
860
|
+
itemTitle: candidate.itemTitle,
|
|
861
|
+
itemId: candidate.itemId,
|
|
862
|
+
itemCategory: candidate.itemCategory,
|
|
863
|
+
fieldLabel: candidate.fieldLabel,
|
|
864
|
+
fieldId: candidate.fieldId,
|
|
865
|
+
fieldType: candidate.fieldType,
|
|
866
|
+
fieldPurpose: candidate.fieldPurpose,
|
|
867
|
+
suggestedEnv: candidate.suggestedEnv,
|
|
868
|
+
companionFields: (candidate.companionFields || []).map((field) => ({
|
|
869
|
+
name: onePasswordHandleName(field.itemTitle, field.fieldLabel),
|
|
870
|
+
type: field.secretType,
|
|
871
|
+
itemTitle: field.itemTitle,
|
|
872
|
+
itemId: field.itemId,
|
|
873
|
+
itemCategory: field.itemCategory,
|
|
874
|
+
fieldLabel: field.fieldLabel,
|
|
875
|
+
fieldId: field.fieldId,
|
|
876
|
+
fieldType: field.fieldType,
|
|
877
|
+
fieldPurpose: field.fieldPurpose,
|
|
878
|
+
suggestedEnv: field.suggestedEnv
|
|
879
|
+
}))
|
|
880
|
+
}))
|
|
881
|
+
});
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const imported = [];
|
|
885
|
+
const includeCompanions = hasFlag(flags, "include-companions");
|
|
886
|
+
const toImport = [];
|
|
887
|
+
const seenRefs = new Set();
|
|
888
|
+
for (const candidate of candidates) {
|
|
889
|
+
toImport.push(candidate);
|
|
890
|
+
seenRefs.add(candidate.reference);
|
|
891
|
+
if (!includeCompanions) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
for (const companion of candidate.companionFields || []) {
|
|
895
|
+
if (seenRefs.has(companion.reference)) {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
seenRefs.add(companion.reference);
|
|
899
|
+
toImport.push(companion);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
for (const candidate of toImport) {
|
|
903
|
+
const record = await store.addOnePasswordReference({
|
|
904
|
+
name: onePasswordHandleName(candidate.itemTitle, candidate.fieldLabel),
|
|
905
|
+
type: candidate.secretType,
|
|
906
|
+
reference: candidate.reference,
|
|
907
|
+
source: `onepassword:${vault}`,
|
|
908
|
+
policy: {
|
|
909
|
+
injectEnv: getFlag(flags, "inject-env") || candidate.suggestedEnv,
|
|
910
|
+
allowedCommands: getFlagList(flags, "allow-command"),
|
|
911
|
+
maxOutputBytes: numericFlag(flags, "max-output-bytes", 16_384)
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
imported.push({
|
|
915
|
+
handle: record.handle,
|
|
916
|
+
name: record.name,
|
|
917
|
+
type: record.type,
|
|
918
|
+
backend: record.backend,
|
|
919
|
+
provider: record.provider,
|
|
920
|
+
itemTitle: candidate.itemTitle,
|
|
921
|
+
fieldLabel: candidate.fieldLabel,
|
|
922
|
+
suggestedEnv: candidate.suggestedEnv,
|
|
923
|
+
policy: record.policy
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
printJson({ vault, includeCompanions, importedCount: imported.length, imported });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (action === "capture") {
|
|
930
|
+
const vault = getFlag(flags, "vault") || "Dev";
|
|
931
|
+
const text = await readStdinValue(flags, "text-stdin", "text to scan");
|
|
932
|
+
const result = await scanTextToOnePassword(store, text, {
|
|
933
|
+
vault,
|
|
934
|
+
source: getFlag(flags, "source") || "onepassword-capture",
|
|
935
|
+
defaultName: getFlag(flags, "name"),
|
|
936
|
+
policy: {
|
|
937
|
+
injectEnv: getFlag(flags, "inject-env"),
|
|
938
|
+
allowedCommands: getFlagList(flags, "allow-command"),
|
|
939
|
+
maxOutputBytes: numericFlag(flags, "max-output-bytes", 16_384)
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
printJson({
|
|
943
|
+
vault,
|
|
944
|
+
backend: "onepassword",
|
|
945
|
+
capturedCount: result.findings.length,
|
|
946
|
+
tokenizedText: result.tokenizedText,
|
|
947
|
+
findings: result.findings
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
throw new Error("onepassword requires status, import, or capture.");
|
|
952
|
+
}
|
|
953
|
+
async function handleSshCommand(store, action, handleArg, flags) {
|
|
954
|
+
if (action === "close") {
|
|
955
|
+
const handle = handleArg || requireFlag(flags, "handle");
|
|
956
|
+
printJson(await closeOwnedSshSession({
|
|
957
|
+
handle,
|
|
958
|
+
target: requireFlag(flags, "target"),
|
|
959
|
+
port: numericFlag(flags, "port", 22),
|
|
960
|
+
home: store.home
|
|
961
|
+
}));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (action === "run" && getFlag(flags, "request-id")) {
|
|
965
|
+
printJson(await executeApprovedRequest(store, requireFlag(flags, "request-id")));
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (action === "request" || action === "run") {
|
|
969
|
+
const handle = handleArg || requireFlag(flags, "handle");
|
|
970
|
+
const secret = await store.getSecretRecord(handle);
|
|
971
|
+
const remoteArgs = commandArgsFromFlags(flags);
|
|
972
|
+
const sshAction = buildSshSessionAction({
|
|
973
|
+
target: requireFlag(flags, "target"),
|
|
974
|
+
port: numericFlag(flags, "port", 22),
|
|
975
|
+
args: remoteArgs,
|
|
976
|
+
injectEnv: getFlag(flags, "inject-env") || defaultSshInjectEnv(secret),
|
|
977
|
+
workingDir: getFlag(flags, "cwd"),
|
|
978
|
+
timeoutMs: numericFlag(flags, "timeout-ms", 30_000)
|
|
979
|
+
});
|
|
980
|
+
const request = await store.createRequest(handle, sshAction, getFlag(flags, "reason") || "s-gw-owned SSH session request");
|
|
981
|
+
if (action === "request" || request.state !== "approved") {
|
|
982
|
+
printJson({
|
|
983
|
+
approvalRequired: request.state !== "approved",
|
|
984
|
+
localApprovalCommand: request.state === "approved" ? undefined : `s-gw approve ${request.id}`,
|
|
985
|
+
localRunCommand: `s-gw ssh run --request-id ${request.id}`,
|
|
986
|
+
request
|
|
987
|
+
});
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
printJson(await executeApprovedRequest(store, request.id));
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
throw new Error("ssh requires request, run, or close.");
|
|
994
|
+
}
|
|
995
|
+
async function handleAwsCommand(store, action, flags, positionalArgs = []) {
|
|
996
|
+
const handles = await resolveAwsHandles(store, flags);
|
|
997
|
+
const wrapper = getFlag(flags, "wrapper") || chooseAwsWrapper(handles.secret, handles.access);
|
|
998
|
+
const awsArgs = positionalArgs.length > 0 ? positionalArgs : commandArgsFromFlags(flags);
|
|
999
|
+
if (!action || action === "plan" || action === "path") {
|
|
1000
|
+
const sampleArgs = awsArgs.length > 0 ? awsArgs : ["sts", "get-caller-identity"];
|
|
1001
|
+
printJson({
|
|
1002
|
+
ok: true,
|
|
1003
|
+
wrapper,
|
|
1004
|
+
secretHandle: handles.secret.handle,
|
|
1005
|
+
secretEnv: handles.secret.policy.injectEnv || "AWS_SECRET_ACCESS_KEY",
|
|
1006
|
+
accessKeyHandle: handles.access.handle,
|
|
1007
|
+
accessKeyEnv: handles.access.policy.injectEnv || "AWS_ACCESS_KEY_ID",
|
|
1008
|
+
sampleRequestCommand: awsCommandLine("request", sampleArgs),
|
|
1009
|
+
sampleRunCommand: awsCommandLine("run", sampleArgs)
|
|
1010
|
+
});
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (action !== "request" && action !== "run") {
|
|
1014
|
+
throw new Error("aws requires request, run, or plan.");
|
|
1015
|
+
}
|
|
1016
|
+
if (awsArgs.length === 0) {
|
|
1017
|
+
throw new Error("aws request/run needs AWS CLI arguments after `--`, for example `s-gw aws run -- sts get-caller-identity`.");
|
|
1018
|
+
}
|
|
1019
|
+
const request = await createAwsRequest(store, handles, wrapper, awsArgs, flags);
|
|
1020
|
+
const response = awsRequestResponse(request, wrapper, awsArgs, handles);
|
|
1021
|
+
if (action === "request" || request.state !== "approved") {
|
|
1022
|
+
printJson(response);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const summary = await executeApprovedRequest(store, request.id);
|
|
1026
|
+
if (hasFlag(flags, "raw")) {
|
|
1027
|
+
process.stdout.write(summary.stdout);
|
|
1028
|
+
if (summary.stderr) {
|
|
1029
|
+
process.stderr.write(summary.stderr);
|
|
1030
|
+
}
|
|
1031
|
+
process.exitCode = summary.exitCode ?? (summary.signal ? 1 : 0);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
printJson({
|
|
1035
|
+
...response,
|
|
1036
|
+
summary
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
async function resolveAwsHandles(store, flags) {
|
|
1040
|
+
const handles = await store.listHandles();
|
|
1041
|
+
const secretHandle = getFlag(flags, "secret-handle");
|
|
1042
|
+
const accessHandle = getFlag(flags, "access-handle");
|
|
1043
|
+
const secret = secretHandle
|
|
1044
|
+
? findHandle(handles, secretHandle, "--secret-handle")
|
|
1045
|
+
: findAwsSecretHandle(handles);
|
|
1046
|
+
const access = accessHandle
|
|
1047
|
+
? findHandle(handles, accessHandle, "--access-handle")
|
|
1048
|
+
: findAwsAccessHandle(handles, secret.handle);
|
|
1049
|
+
if (secret.handle === access.handle) {
|
|
1050
|
+
throw new Error("AWS secret and access-key handles must be different.");
|
|
1051
|
+
}
|
|
1052
|
+
return { secret, access };
|
|
1053
|
+
}
|
|
1054
|
+
function findHandle(handles, handle, label) {
|
|
1055
|
+
const found = handles.find((item) => item.handle === handle);
|
|
1056
|
+
if (!found) {
|
|
1057
|
+
throw new Error(`Unknown ${label}: ${handle}`);
|
|
1058
|
+
}
|
|
1059
|
+
return found;
|
|
1060
|
+
}
|
|
1061
|
+
function findAwsSecretHandle(handles) {
|
|
1062
|
+
const exact = handles.find((item) => item.policy.injectEnv === "SGW_AWS_DEV_CREDENTIAL")
|
|
1063
|
+
|| handles.find((item) => item.policy.injectEnv === "AWS_SECRET_ACCESS_KEY");
|
|
1064
|
+
if (exact) {
|
|
1065
|
+
return exact;
|
|
1066
|
+
}
|
|
1067
|
+
const named = handles.find((item) => {
|
|
1068
|
+
const name = item.name.toLowerCase();
|
|
1069
|
+
return name.includes("aws") && !name.includes("access key id") && !name.includes("username");
|
|
1070
|
+
});
|
|
1071
|
+
if (named) {
|
|
1072
|
+
return named;
|
|
1073
|
+
}
|
|
1074
|
+
throw new Error("No AWS secret handle found. Enroll or import AWS-dev through s-gw, then retry.");
|
|
1075
|
+
}
|
|
1076
|
+
function findAwsAccessHandle(handles, secretHandle) {
|
|
1077
|
+
const exact = handles.find((item) => item.handle !== secretHandle && item.policy.injectEnv === "SGW_AWS_DEV_ACCESS_KEY_ID")
|
|
1078
|
+
|| handles.find((item) => item.handle !== secretHandle && item.policy.injectEnv === "AWS_ACCESS_KEY_ID");
|
|
1079
|
+
if (exact) {
|
|
1080
|
+
return exact;
|
|
1081
|
+
}
|
|
1082
|
+
const named = handles.find((item) => {
|
|
1083
|
+
const name = item.name.toLowerCase();
|
|
1084
|
+
return item.handle !== secretHandle && name.includes("aws") && (name.includes("access key id") || name.includes("username"));
|
|
1085
|
+
});
|
|
1086
|
+
if (named) {
|
|
1087
|
+
return named;
|
|
1088
|
+
}
|
|
1089
|
+
throw new Error("No AWS access-key-id handle found. Import companion fields with `s-gw onepassword import --include-companions`, then retry.");
|
|
1090
|
+
}
|
|
1091
|
+
function chooseAwsWrapper(secret, access) {
|
|
1092
|
+
const secretAllowed = secret.policy.allowedCommands || [];
|
|
1093
|
+
const accessAllowed = new Set(access.policy.allowedCommands || []);
|
|
1094
|
+
const common = secretAllowed.filter((command) => accessAllowed.has(command));
|
|
1095
|
+
const wrapper = common.find((command) => path.basename(command).includes("aws"))
|
|
1096
|
+
|| common[0];
|
|
1097
|
+
if (!wrapper) {
|
|
1098
|
+
throw new Error("No shared AWS wrapper command is allowed for the AWS secret/access handles. Pass --wrapper or update both handle policies.");
|
|
1099
|
+
}
|
|
1100
|
+
return wrapper;
|
|
1101
|
+
}
|
|
1102
|
+
async function createAwsRequest(store, handles, wrapper, awsArgs, flags) {
|
|
1103
|
+
const action = buildEnvCommandAction({
|
|
1104
|
+
command: wrapper,
|
|
1105
|
+
args: awsArgs,
|
|
1106
|
+
injectEnv: handles.secret.policy.injectEnv || "AWS_SECRET_ACCESS_KEY",
|
|
1107
|
+
env: [{
|
|
1108
|
+
handle: handles.access.handle,
|
|
1109
|
+
injectEnv: handles.access.policy.injectEnv || "AWS_ACCESS_KEY_ID"
|
|
1110
|
+
}],
|
|
1111
|
+
workingDir: getFlag(flags, "cwd"),
|
|
1112
|
+
timeoutMs: numericFlag(flags, "timeout-ms", 30_000)
|
|
1113
|
+
});
|
|
1114
|
+
return store.createRequest(handles.secret.handle, action, getFlag(flags, "reason") || "s-gw AWS command request");
|
|
1115
|
+
}
|
|
1116
|
+
function awsRequestResponse(request, wrapper, awsArgs, handles) {
|
|
1117
|
+
return {
|
|
1118
|
+
approvalRequired: request.state !== "approved",
|
|
1119
|
+
localApprovalCommand: request.state === "approved"
|
|
1120
|
+
? undefined
|
|
1121
|
+
: `s-gw approve ${request.id} --mode timed-session --duration 8h --agent-scope any-agent`,
|
|
1122
|
+
localRunCommand: `s-gw execute ${request.id}`,
|
|
1123
|
+
repeatCommand: awsCommandLine("run", awsArgs),
|
|
1124
|
+
wrapper,
|
|
1125
|
+
secretHandle: handles.secret.handle,
|
|
1126
|
+
accessKeyHandle: handles.access.handle,
|
|
1127
|
+
request
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function awsCommandLine(action, args, options = {}) {
|
|
1131
|
+
const parts = ["s-gw", "aws", action];
|
|
1132
|
+
if (options.wrapper) {
|
|
1133
|
+
parts.push("--wrapper", options.wrapper);
|
|
1134
|
+
}
|
|
1135
|
+
return [...parts.map(shellArg), "--", ...args.map(shellArg)].join(" ");
|
|
1136
|
+
}
|
|
1137
|
+
function shellArg(value) {
|
|
1138
|
+
if (/^[A-Za-z0-9_./:=@%+-]+$/.test(value)) {
|
|
1139
|
+
return value;
|
|
1140
|
+
}
|
|
1141
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
1142
|
+
}
|
|
1143
|
+
async function handleApprovalCommand(store, action, target, flags) {
|
|
1144
|
+
if (action === "policy" || action === "policies") {
|
|
1145
|
+
await handleApprovalPolicyCommand(store, target, flags);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (!action || action === "settings" || action === "get") {
|
|
1149
|
+
printJson(await store.getApprovalSettings());
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (action === "grants" || action === "list") {
|
|
1153
|
+
printJson(await store.listApprovalGrants());
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (action === "revoke") {
|
|
1157
|
+
const id = target || requireFlag(flags, "id");
|
|
1158
|
+
printJson(await store.revokeApprovalGrant(id));
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (action === "clear") {
|
|
1162
|
+
printJson(await store.clearApprovalGrants());
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (action === "set") {
|
|
1166
|
+
const mode = approvalMode(requireFlag(flags, "mode"));
|
|
1167
|
+
const duration = getFlag(flags, "duration") || getFlag(flags, "duration-ms");
|
|
1168
|
+
printJson(await store.setApprovalSettings({
|
|
1169
|
+
mode,
|
|
1170
|
+
durationMs: duration ? parseDurationMs(duration) : undefined
|
|
1171
|
+
}));
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
throw new Error("approval requires settings/get, grants/list, policy, revoke, clear, or set.");
|
|
1175
|
+
}
|
|
1176
|
+
async function handleApprovalPolicyCommand(store, action, flags) {
|
|
1177
|
+
if (!action || action === "list") {
|
|
1178
|
+
printJson(await store.listApprovalPolicyRules());
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (action === "add") {
|
|
1182
|
+
const duration = getFlag(flags, "duration") || getFlag(flags, "duration-ms");
|
|
1183
|
+
printJson(await store.addApprovalPolicyRule({
|
|
1184
|
+
name: getFlag(flags, "name"),
|
|
1185
|
+
enabled: !hasFlag(flags, "disabled"),
|
|
1186
|
+
priority: optionalNumericFlag(flags, "priority"),
|
|
1187
|
+
decision: approvalPolicyDecision(getFlag(flags, "decision") || "ask"),
|
|
1188
|
+
durationMs: duration ? parseDurationMs(duration) : undefined,
|
|
1189
|
+
expiresAt: getFlag(flags, "expires-at"),
|
|
1190
|
+
conditions: {
|
|
1191
|
+
handles: getFlagList(flags, "handle"),
|
|
1192
|
+
secretTypes: getFlagList(flags, "type").map(secretType),
|
|
1193
|
+
providers: getFlagList(flags, "provider"),
|
|
1194
|
+
minSeverity: optionalSecretSeverity(getFlag(flags, "min-severity")),
|
|
1195
|
+
agents: getFlagList(flags, "agent"),
|
|
1196
|
+
actionKinds: getFlagList(flags, "action-kind").map(approvalPolicyActionKind),
|
|
1197
|
+
commands: getFlagList(flags, "command"),
|
|
1198
|
+
injectEnvs: getFlagList(flags, "inject-env"),
|
|
1199
|
+
workingDirs: getFlagList(flags, "working-dir").concat(getFlagList(flags, "cwd")),
|
|
1200
|
+
sshTargets: getFlagList(flags, "ssh-target").concat(getFlagList(flags, "target")),
|
|
1201
|
+
sshPorts: getFlagList(flags, "ssh-port").concat(getFlagList(flags, "port")).map((item) => Number(item))
|
|
1202
|
+
}
|
|
1203
|
+
}));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (action === "delete" || action === "remove") {
|
|
1207
|
+
printJson(await store.deleteApprovalPolicyRule(requireFlag(flags, "id")));
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (action === "enable" || action === "disable") {
|
|
1211
|
+
const id = requireFlag(flags, "id");
|
|
1212
|
+
printJson(await store.setApprovalPolicyRuleEnabled(id, action === "enable"));
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
throw new Error("approval policy requires list, add, delete, enable, or disable.");
|
|
1216
|
+
}
|
|
1217
|
+
function onePasswordHandleName(itemTitle, fieldLabel) {
|
|
1218
|
+
return `${itemTitle} ${fieldLabel}`.trim();
|
|
1219
|
+
}
|
|
1220
|
+
async function handleAppCommand(action, flags) {
|
|
1221
|
+
if (action === "app-path") {
|
|
1222
|
+
const layout = getPackageLayout();
|
|
1223
|
+
process.stdout.write(`${process.platform === "win32" ? layout.windowsClientLauncherPath : layout.macAppPath}\n`);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (action === "open") {
|
|
1227
|
+
if (process.platform === "win32") {
|
|
1228
|
+
printJson(openWindowsClient({
|
|
1229
|
+
consoleUrl: getFlag(flags, "console-url"),
|
|
1230
|
+
port: numericFlag(flags, "port", 8718)
|
|
1231
|
+
}));
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
printJson(openMacApp({
|
|
1235
|
+
consoleUrl: getFlag(flags, "console-url"),
|
|
1236
|
+
port: numericFlag(flags, "port", 8718)
|
|
1237
|
+
}));
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
throw new Error("app requires app-path or open.");
|
|
1241
|
+
}
|
|
1242
|
+
async function handleGuardCommand(store, action, agent, flags) {
|
|
1243
|
+
if (!action || action === "status") {
|
|
1244
|
+
printJson(guardStatus());
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (action === "run") {
|
|
1248
|
+
await handleGuardRun(store, agent || getFlag(flags, "agent"), flags);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
throw new Error("guard requires status or run.");
|
|
1252
|
+
}
|
|
1253
|
+
async function handleGuardRun(store, agent, flags) {
|
|
1254
|
+
if (!agent) {
|
|
1255
|
+
throw new Error("guard run requires an agent name.");
|
|
1256
|
+
}
|
|
1257
|
+
const options = {
|
|
1258
|
+
agent,
|
|
1259
|
+
command: getFlag(flags, "command"),
|
|
1260
|
+
args: getFlagList(flags, "--"),
|
|
1261
|
+
cwd: getFlag(flags, "cwd") || process.cwd(),
|
|
1262
|
+
extraEnv: parseEnvFlags(getFlagList(flags, "env")),
|
|
1263
|
+
scrubEnv: !hasFlag(flags, "no-scrub-env"),
|
|
1264
|
+
allowedCommands: getFlagList(flags, "allow-command")
|
|
1265
|
+
};
|
|
1266
|
+
if (hasFlag(flags, "dry-run")) {
|
|
1267
|
+
const prepared = await prepareGuardedRun(store, {
|
|
1268
|
+
...options,
|
|
1269
|
+
persist: false
|
|
1270
|
+
});
|
|
1271
|
+
printJson(prepared.plan);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const code = await runGuardedAgent(store, options);
|
|
1275
|
+
process.exitCode = code;
|
|
1276
|
+
}
|
|
1277
|
+
function openPreferredUi(port, consoleUrl) {
|
|
1278
|
+
try {
|
|
1279
|
+
if (process.platform === "win32") {
|
|
1280
|
+
const opened = openWindowsClient({ port, consoleUrl });
|
|
1281
|
+
return { kind: "windows-client", ...opened };
|
|
1282
|
+
}
|
|
1283
|
+
const opened = openMacApp({ port, consoleUrl });
|
|
1284
|
+
return { kind: "mac-app", ...opened };
|
|
1285
|
+
}
|
|
1286
|
+
catch {
|
|
1287
|
+
openBrowser(consoleUrl);
|
|
1288
|
+
return { kind: "web-console", consoleUrl };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function shouldOpenUi(flags) {
|
|
1292
|
+
return !hasFlag(flags, "no-open-console") && !hasFlag(flags, "no-open-app");
|
|
1293
|
+
}
|
|
1294
|
+
function openBrowser(url) {
|
|
1295
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1296
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1297
|
+
const child = spawn(command, args, {
|
|
1298
|
+
detached: true,
|
|
1299
|
+
stdio: "ignore"
|
|
1300
|
+
});
|
|
1301
|
+
child.unref();
|
|
1302
|
+
}
|
|
1303
|
+
async function waitForever() {
|
|
1304
|
+
await new Promise((resolve) => {
|
|
1305
|
+
process.once("SIGINT", resolve);
|
|
1306
|
+
process.once("SIGTERM", resolve);
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
function isAddressInUse(error) {
|
|
1310
|
+
return error?.code === "EADDRINUSE";
|
|
1311
|
+
}
|
|
1312
|
+
function printJson(value) {
|
|
1313
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
1314
|
+
}
|
|
1315
|
+
function printHelp() {
|
|
1316
|
+
process.stdout.write(`s-gw local credential gateway
|
|
1317
|
+
|
|
1318
|
+
Commands:
|
|
1319
|
+
s-gw init
|
|
1320
|
+
s-gw setup [--port 8718] [--passphrase-stdin] [--menubar-count pending|credentials|none] [--no-open-app] [--no-service] [--no-menubar]
|
|
1321
|
+
s-gw status
|
|
1322
|
+
s-gw start [--port 8718] [--no-open-app]
|
|
1323
|
+
s-gw stop
|
|
1324
|
+
s-gw doctor
|
|
1325
|
+
s-gw console [--host 127.0.0.1] [--port 8718] [--no-open]
|
|
1326
|
+
s-gw app app-path
|
|
1327
|
+
s-gw app open [--port 8718] [--console-url URL]
|
|
1328
|
+
s-gw guard status
|
|
1329
|
+
s-gw guard run AGENT [--dry-run] [--command CMD] [--env KEY=VALUE] [--allow-command CMD] [--] [agent args...]
|
|
1330
|
+
s-gw run AGENT [--dry-run] [--command CMD] [--env KEY=VALUE] [--allow-command CMD] [--] [agent args...]
|
|
1331
|
+
s-gw service install [--port 8718] [--start]
|
|
1332
|
+
s-gw service start|stop|status|uninstall
|
|
1333
|
+
s-gw menubar app-path
|
|
1334
|
+
s-gw menubar open [--port 8718] [--console-url URL] [--count pending|credentials|none] [--show] [--no-notify]
|
|
1335
|
+
s-gw menubar install [--port 8718] [--console-url URL] [--count pending|credentials|none] [--start] [--no-notify]
|
|
1336
|
+
s-gw menubar start|stop|status|uninstall
|
|
1337
|
+
s-gw helper open
|
|
1338
|
+
s-gw unlock status
|
|
1339
|
+
s-gw unlock keychain set --value-stdin
|
|
1340
|
+
s-gw unlock keychain delete
|
|
1341
|
+
s-gw secret add --name NAME --type api-token --value-stdin --inject-env ENV --allow-command CMD
|
|
1342
|
+
s-gw secret add-keychain --name NAME --type api-token --value-stdin --inject-env ENV --allow-command CMD [--service SERVICE]
|
|
1343
|
+
s-gw secret add-1password --name NAME --type api-token --ref op://vault/item/field --inject-env ENV --allow-command CMD [--verify]
|
|
1344
|
+
s-gw secret list
|
|
1345
|
+
s-gw secret delete HANDLE
|
|
1346
|
+
s-gw secret allow-command HANDLE [--command s-gw:ssh-session]
|
|
1347
|
+
s-gw onepassword status
|
|
1348
|
+
s-gw onepassword import [--vault Dev] [--dry-run] [--include-companions] [--allow-command CMD]
|
|
1349
|
+
s-gw onepassword capture --text-stdin [--vault Dev] [--name NAME] [--inject-env ENV] [--allow-command CMD]
|
|
1350
|
+
s-gw aws plan|request|run [--wrapper CMD] [--timeout-ms 0] [--raw] -- AWS_ARGS...
|
|
1351
|
+
s-gw ssh request HANDLE --target user@host [--port 22] [--arg VALUE]
|
|
1352
|
+
s-gw ssh run HANDLE --target user@host [--port 22] [--] [remote command...]
|
|
1353
|
+
s-gw ssh run --request-id REQUEST_ID
|
|
1354
|
+
s-gw ssh close HANDLE --target user@host [--port 22]
|
|
1355
|
+
s-gw approval settings
|
|
1356
|
+
s-gw approval set --mode per-transaction|timed-session|login-session|always [--duration 15m]
|
|
1357
|
+
s-gw approval grants
|
|
1358
|
+
s-gw approval policy list
|
|
1359
|
+
s-gw approval policy add --name NAME --decision allow|ask|deny [--handle HANDLE] [--agent Codex] [--command /path/to/tool] [--action-kind env_command|ssh_session] [--duration 8h]
|
|
1360
|
+
s-gw approval policy delete --id POLICY_ID
|
|
1361
|
+
s-gw approval policy enable|disable --id POLICY_ID
|
|
1362
|
+
s-gw approval revoke GRANT_ID
|
|
1363
|
+
s-gw approval clear
|
|
1364
|
+
s-gw scan-file PATH [--preview] [--backend keychain|local]
|
|
1365
|
+
s-gw agent list
|
|
1366
|
+
s-gw agent show AGENT [--command CMD] [--arg VALUE] [--env KEY=VALUE]
|
|
1367
|
+
s-gw agent codeguard-plan AGENT
|
|
1368
|
+
s-gw agent mcp-snippet AGENT [--command CMD] [--arg VALUE] [--env KEY=VALUE]
|
|
1369
|
+
s-gw request env-command HANDLE --command CMD --inject-env ENV [--with-env ENV=HANDLE] [--arg VALUE] [--args-json JSON] [--timeout-ms 0]
|
|
1370
|
+
s-gw requests [--state pending] [--active] [--limit 100] [--all]
|
|
1371
|
+
s-gw requests --recover [REQUEST_ID]
|
|
1372
|
+
s-gw requests cleanup [--pending-ttl-ms 86400000] [--approved-ttl-ms 3600000] [--no-dedupe]
|
|
1373
|
+
s-gw execute-next [--handle HANDLE] [--kind env_command|ssh_session] [--command CMD]
|
|
1374
|
+
s-gw store backups
|
|
1375
|
+
s-gw approve REQUEST_ID [--mode per-transaction|timed-session|login-session|always] [--duration 8h] [--agent-scope same-agent|any-agent]
|
|
1376
|
+
s-gw deny REQUEST_ID
|
|
1377
|
+
s-gw execute REQUEST_ID
|
|
1378
|
+
`);
|
|
1379
|
+
}
|
|
1380
|
+
main().catch((error) => {
|
|
1381
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1382
|
+
process.stderr.write(`s-gw error: ${message}\n`);
|
|
1383
|
+
process.exitCode = 1;
|
|
1384
|
+
});
|
|
1385
|
+
//# sourceMappingURL=cli.js.map
|