@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/executor.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { assertActionAllowed } from "./store.js";
|
|
6
|
+
import { sanitizeKnownSecrets } from "./scanner.js";
|
|
7
|
+
import { runOwnedSshSession } from "./ssh.js";
|
|
8
|
+
export async function executeApprovedRequest(store, requestId, options = {}) {
|
|
9
|
+
const request = await store.claimApprovedRequest(requestId);
|
|
10
|
+
try {
|
|
11
|
+
const secretRecord = await store.getSecretRecord(request.handle);
|
|
12
|
+
assertActionAllowed(secretRecord, request.action);
|
|
13
|
+
const secretValue = await store.revealSecretForLocalUse(request.handle, request);
|
|
14
|
+
const extraSecrets = await resolveExtraSecrets(store, request);
|
|
15
|
+
const summary = request.action.kind === "ssh_session"
|
|
16
|
+
? await runOwnedSshSession(request, secretRecord, secretValue, store.home)
|
|
17
|
+
: await runEnvCommand(request, secretRecord, secretValue, extraSecrets, options);
|
|
18
|
+
await store.markExecuted(requestId, summary);
|
|
19
|
+
return summary;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
await store.markFailed(requestId, message);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function runEnvCommand(request, secretRecord, secretValue, extraSecrets, options) {
|
|
28
|
+
const engine = executionEngine(options.engine);
|
|
29
|
+
const coreBinary = options.coreBinary || rustCoreBinary();
|
|
30
|
+
if (engine !== "typescript" && existsSync(coreBinary)) {
|
|
31
|
+
return runRustEnvCommand(coreBinary, request, secretRecord, secretValue, extraSecrets);
|
|
32
|
+
}
|
|
33
|
+
if (engine === "rust") {
|
|
34
|
+
throw new Error(`Rust execution core is unavailable: ${coreBinary}`);
|
|
35
|
+
}
|
|
36
|
+
return runTypeScriptEnvCommand(request, secretRecord, secretValue, extraSecrets);
|
|
37
|
+
}
|
|
38
|
+
async function runRustEnvCommand(coreBinary, request, secretRecord, secretValue, extraSecrets) {
|
|
39
|
+
const maxOutput = secretRecord.policy.maxOutputBytes || 16_384;
|
|
40
|
+
const child = spawn(coreBinary, ["execute"], {
|
|
41
|
+
env: buildCoreEnv(),
|
|
42
|
+
shell: false,
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
44
|
+
});
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let stderr = "";
|
|
47
|
+
const responseLimit = Math.max(1_048_576, maxOutput * 4);
|
|
48
|
+
child.stdout.on("data", (chunk) => {
|
|
49
|
+
stdout = appendBounded(stdout, chunk.toString("utf8"), responseLimit);
|
|
50
|
+
});
|
|
51
|
+
child.stderr.on("data", (chunk) => {
|
|
52
|
+
stderr = appendBounded(stderr, chunk.toString("utf8"), responseLimit);
|
|
53
|
+
});
|
|
54
|
+
const completion = new Promise((resolve, reject) => {
|
|
55
|
+
child.once("error", reject);
|
|
56
|
+
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
57
|
+
});
|
|
58
|
+
child.stdin.end(JSON.stringify({
|
|
59
|
+
version: 1,
|
|
60
|
+
requestId: request.id,
|
|
61
|
+
handle: request.handle,
|
|
62
|
+
command: request.action.command,
|
|
63
|
+
args: request.action.args,
|
|
64
|
+
injectEnv: request.action.injectEnv,
|
|
65
|
+
secretValue,
|
|
66
|
+
env: extraSecrets.map((item) => ({
|
|
67
|
+
handle: item.handle,
|
|
68
|
+
injectEnv: item.injectEnv,
|
|
69
|
+
value: item.value
|
|
70
|
+
})),
|
|
71
|
+
workingDir: request.action.workingDir,
|
|
72
|
+
timeoutMs: request.action.timeoutMs,
|
|
73
|
+
maxOutputBytes: maxOutput
|
|
74
|
+
}));
|
|
75
|
+
const status = await completion;
|
|
76
|
+
if (status.code !== 0) {
|
|
77
|
+
throw new Error(stderr.trim() || `Rust execution core exited ${status.code ?? status.signal ?? "unexpectedly"}.`);
|
|
78
|
+
}
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(stdout);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error("Rust execution core returned an invalid response.");
|
|
85
|
+
}
|
|
86
|
+
const summary = parseCoreSummary(parsed, request);
|
|
87
|
+
const secretValues = [secretValue, ...extraSecrets.map((item) => item.value)];
|
|
88
|
+
if (secretValues.some((value) => summary.stdout.includes(value) || summary.stderr.includes(value))) {
|
|
89
|
+
throw new Error("Rust execution core returned unsanitized output.");
|
|
90
|
+
}
|
|
91
|
+
const expectedProof = proofFor(request, summary.stdout, summary.stderr);
|
|
92
|
+
if (summary.proof !== expectedProof) {
|
|
93
|
+
throw new Error("Rust execution core returned an invalid execution proof.");
|
|
94
|
+
}
|
|
95
|
+
return summary;
|
|
96
|
+
}
|
|
97
|
+
function parseCoreSummary(value, request) {
|
|
98
|
+
if (!value || typeof value !== "object") {
|
|
99
|
+
throw new Error("Rust execution core returned an invalid summary.");
|
|
100
|
+
}
|
|
101
|
+
const summary = value;
|
|
102
|
+
const validExit = summary.exitCode === null || Number.isInteger(summary.exitCode);
|
|
103
|
+
const validSignal = summary.signal === null || typeof summary.signal === "string";
|
|
104
|
+
if (!validExit
|
|
105
|
+
|| !validSignal
|
|
106
|
+
|| typeof summary.stdout !== "string"
|
|
107
|
+
|| typeof summary.stderr !== "string"
|
|
108
|
+
|| typeof summary.proof !== "string"
|
|
109
|
+
|| typeof summary.durationMs !== "number"
|
|
110
|
+
|| !Number.isFinite(summary.durationMs)
|
|
111
|
+
|| summary.durationMs < 0
|
|
112
|
+
|| summary.timeoutMs !== request.action.timeoutMs
|
|
113
|
+
|| typeof summary.timedOut !== "boolean"
|
|
114
|
+
|| typeof summary.sanitized !== "boolean") {
|
|
115
|
+
throw new Error("Rust execution core returned an invalid summary.");
|
|
116
|
+
}
|
|
117
|
+
return summary;
|
|
118
|
+
}
|
|
119
|
+
function executionEngine(override) {
|
|
120
|
+
const configured = override || process.env.SGW_EXECUTION_ENGINE?.trim().toLowerCase() || "auto";
|
|
121
|
+
if (configured === "auto" || configured === "rust" || configured === "typescript") {
|
|
122
|
+
return configured;
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Unsupported SGW_EXECUTION_ENGINE value: ${configured}`);
|
|
125
|
+
}
|
|
126
|
+
function rustCoreBinary() {
|
|
127
|
+
const extension = process.platform === "win32" ? ".exe" : "";
|
|
128
|
+
return fileURLToPath(new URL(`../dist/native/s-gw-core${extension}`, import.meta.url));
|
|
129
|
+
}
|
|
130
|
+
function buildCoreEnv() {
|
|
131
|
+
const env = {};
|
|
132
|
+
const copyKeys = [
|
|
133
|
+
"HOME",
|
|
134
|
+
"LANG",
|
|
135
|
+
"LC_ALL",
|
|
136
|
+
"LC_CTYPE",
|
|
137
|
+
"LC_MESSAGES",
|
|
138
|
+
"LOGNAME",
|
|
139
|
+
"NO_COLOR",
|
|
140
|
+
"PATH",
|
|
141
|
+
"SHELL",
|
|
142
|
+
"SYSTEMROOT",
|
|
143
|
+
"TERM",
|
|
144
|
+
"TMPDIR",
|
|
145
|
+
"USER",
|
|
146
|
+
"USERPROFILE",
|
|
147
|
+
"WINDIR"
|
|
148
|
+
];
|
|
149
|
+
for (const key of copyKeys) {
|
|
150
|
+
const value = process.env[key];
|
|
151
|
+
if (value) {
|
|
152
|
+
env[key] = value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
env.PATH ||= process.platform === "win32"
|
|
156
|
+
? "C:\\Windows\\System32;C:\\Windows"
|
|
157
|
+
: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
158
|
+
return env;
|
|
159
|
+
}
|
|
160
|
+
async function runTypeScriptEnvCommand(request, secretRecord, secretValue, extraSecrets) {
|
|
161
|
+
const started = Date.now();
|
|
162
|
+
const maxOutput = secretRecord.policy.maxOutputBytes || 16_384;
|
|
163
|
+
const secretPairs = [
|
|
164
|
+
{ handle: request.handle, value: secretValue },
|
|
165
|
+
...extraSecrets.map((item) => ({ handle: item.handle, value: item.value }))
|
|
166
|
+
];
|
|
167
|
+
const longestSecretBytes = secretPairs.reduce((max, item) => Math.max(max, Buffer.byteLength(item.value, "utf8")), 0);
|
|
168
|
+
// Capture slightly more than the display cap so a secret that straddles the
|
|
169
|
+
// boundary is still present in full when we sanitize. Without this headroom,
|
|
170
|
+
// truncating raw output first could cut a secret in half and leak the prefix.
|
|
171
|
+
const captureCap = maxOutput + longestSecretBytes;
|
|
172
|
+
const env = buildExecutionEnv(request.action.injectEnv, secretValue, extraSecrets);
|
|
173
|
+
const child = spawn(request.action.command, request.action.args, {
|
|
174
|
+
cwd: request.action.workingDir,
|
|
175
|
+
env,
|
|
176
|
+
shell: false,
|
|
177
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
178
|
+
});
|
|
179
|
+
let stdout = "";
|
|
180
|
+
let stderr = "";
|
|
181
|
+
let timedOut = false;
|
|
182
|
+
let killTimer;
|
|
183
|
+
const timeout = request.action.timeoutMs > 0
|
|
184
|
+
? setTimeout(() => {
|
|
185
|
+
timedOut = true;
|
|
186
|
+
child.kill("SIGTERM");
|
|
187
|
+
killTimer = setTimeout(() => {
|
|
188
|
+
child.kill("SIGKILL");
|
|
189
|
+
}, 1_500);
|
|
190
|
+
}, request.action.timeoutMs)
|
|
191
|
+
: undefined;
|
|
192
|
+
child.stdout.on("data", (chunk) => {
|
|
193
|
+
stdout = appendBounded(stdout, chunk.toString("utf8"), captureCap);
|
|
194
|
+
});
|
|
195
|
+
child.stderr.on("data", (chunk) => {
|
|
196
|
+
stderr = appendBounded(stderr, chunk.toString("utf8"), captureCap);
|
|
197
|
+
});
|
|
198
|
+
const status = await new Promise((resolve, reject) => {
|
|
199
|
+
child.on("error", reject);
|
|
200
|
+
child.on("close", (code, signal) => resolve({ code, signal }));
|
|
201
|
+
});
|
|
202
|
+
if (timeout) {
|
|
203
|
+
clearTimeout(timeout);
|
|
204
|
+
}
|
|
205
|
+
if (killTimer) {
|
|
206
|
+
clearTimeout(killTimer);
|
|
207
|
+
}
|
|
208
|
+
// Sanitize the full captured output BEFORE applying the display cap. Sanitized
|
|
209
|
+
// text no longer contains the raw secret, so the final byte-cap can never split
|
|
210
|
+
// a credential and leak a partial value to the caller.
|
|
211
|
+
const sanitizedStdout = sanitizeKnownSecrets(stdout, secretPairs);
|
|
212
|
+
const sanitizedStderr = sanitizeKnownSecrets(stderr, secretPairs);
|
|
213
|
+
const cleanStdout = capBytes(sanitizedStdout, maxOutput);
|
|
214
|
+
const cleanStderr = capBytes(sanitizedStderr, maxOutput);
|
|
215
|
+
const summary = {
|
|
216
|
+
exitCode: timedOut ? 124 : status.code,
|
|
217
|
+
signal: status.signal,
|
|
218
|
+
stdout: cleanStdout,
|
|
219
|
+
stderr: cleanStderr,
|
|
220
|
+
proof: proofFor(request, cleanStdout, cleanStderr),
|
|
221
|
+
durationMs: Date.now() - started,
|
|
222
|
+
timeoutMs: request.action.timeoutMs,
|
|
223
|
+
timedOut,
|
|
224
|
+
sanitized: sanitizedStdout !== stdout || sanitizedStderr !== stderr
|
|
225
|
+
};
|
|
226
|
+
return summary;
|
|
227
|
+
}
|
|
228
|
+
async function resolveExtraSecrets(store, request) {
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const binding of request.action.env || []) {
|
|
231
|
+
const secret = await store.getSecretRecord(binding.handle);
|
|
232
|
+
assertActionAllowed(secret, {
|
|
233
|
+
...request.action,
|
|
234
|
+
injectEnv: binding.injectEnv,
|
|
235
|
+
env: []
|
|
236
|
+
});
|
|
237
|
+
out.push({
|
|
238
|
+
handle: binding.handle,
|
|
239
|
+
injectEnv: binding.injectEnv,
|
|
240
|
+
value: await store.revealSecretForLocalUse(binding.handle, request)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
function buildExecutionEnv(injectEnv, secretValue, extraSecrets) {
|
|
246
|
+
const env = {};
|
|
247
|
+
const copyKeys = [
|
|
248
|
+
"HOME",
|
|
249
|
+
"LANG",
|
|
250
|
+
"LC_ALL",
|
|
251
|
+
"LC_CTYPE",
|
|
252
|
+
"LC_MESSAGES",
|
|
253
|
+
"LOGNAME",
|
|
254
|
+
"NO_COLOR",
|
|
255
|
+
"PATH",
|
|
256
|
+
"SHELL",
|
|
257
|
+
"TERM",
|
|
258
|
+
"TMPDIR",
|
|
259
|
+
"USER"
|
|
260
|
+
];
|
|
261
|
+
for (const key of copyKeys) {
|
|
262
|
+
const value = process.env[key];
|
|
263
|
+
if (value) {
|
|
264
|
+
env[key] = value;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
env.PATH ||= "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
268
|
+
env[injectEnv] = secretValue;
|
|
269
|
+
for (const item of extraSecrets) {
|
|
270
|
+
env[item.injectEnv] = item.value;
|
|
271
|
+
}
|
|
272
|
+
return env;
|
|
273
|
+
}
|
|
274
|
+
function appendBounded(current, extra, maxBytes) {
|
|
275
|
+
const combined = current + extra;
|
|
276
|
+
if (Buffer.byteLength(combined, "utf8") <= maxBytes) {
|
|
277
|
+
return combined;
|
|
278
|
+
}
|
|
279
|
+
return capBytes(combined, maxBytes);
|
|
280
|
+
}
|
|
281
|
+
function capBytes(text, maxBytes) {
|
|
282
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) {
|
|
283
|
+
return text;
|
|
284
|
+
}
|
|
285
|
+
return text.slice(0, maxBytes) + "\n<<SGW_OUTPUT_TRUNCATED>>";
|
|
286
|
+
}
|
|
287
|
+
function proofFor(request, stdout, stderr) {
|
|
288
|
+
const digest = createHash("sha256")
|
|
289
|
+
.update(request.id)
|
|
290
|
+
.update(request.handle)
|
|
291
|
+
.update(stdout)
|
|
292
|
+
.update(stderr)
|
|
293
|
+
.digest("base64url")
|
|
294
|
+
.slice(0, 24);
|
|
295
|
+
return `s-gw-proof:${request.id}:${digest}`;
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SecretStore } from "./store.js";
|
|
2
|
+
import { buildSshSessionAction } from "./ssh.js";
|
|
3
|
+
import type { CommandAction, CommandEnvBinding, ScanResult, SecretPolicy } from "./types.js";
|
|
4
|
+
import type { AddKeychainSecretInput, AddSecretInput } from "./store.js";
|
|
5
|
+
export type LocalSecretBackend = "local" | "keychain";
|
|
6
|
+
export interface ScanOptions {
|
|
7
|
+
persist?: boolean;
|
|
8
|
+
source?: string;
|
|
9
|
+
defaultName?: string;
|
|
10
|
+
backend?: LocalSecretBackend;
|
|
11
|
+
}
|
|
12
|
+
export interface OnePasswordScanOptions {
|
|
13
|
+
vault?: string;
|
|
14
|
+
source?: string;
|
|
15
|
+
defaultName?: string;
|
|
16
|
+
policy?: Partial<SecretPolicy>;
|
|
17
|
+
}
|
|
18
|
+
export declare function scanLocalText(store: SecretStore, text: string, options?: ScanOptions): Promise<ScanResult>;
|
|
19
|
+
export declare function addLocalSecret(store: SecretStore, input: AddSecretInput & Pick<AddKeychainSecretInput, "service">, backend?: LocalSecretBackend): Promise<import("./types.js").SecretRecord>;
|
|
20
|
+
export declare function preferredLocalSecretBackend(): LocalSecretBackend;
|
|
21
|
+
export declare function scanTextToOnePassword(store: SecretStore, text: string, options?: OnePasswordScanOptions): Promise<ScanResult>;
|
|
22
|
+
export declare function scanLocalFile(store: SecretStore, filePath: string, options?: ScanOptions): Promise<ScanResult>;
|
|
23
|
+
export declare function buildEnvCommandAction(input: {
|
|
24
|
+
command: string;
|
|
25
|
+
args?: string[];
|
|
26
|
+
injectEnv: string;
|
|
27
|
+
env?: CommandEnvBinding[];
|
|
28
|
+
workingDir?: string;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}): CommandAction;
|
|
31
|
+
export { buildSshSessionAction };
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createOnePasswordSecretReference } from "./onepassword.js";
|
|
4
|
+
import { previewHandle, scanText } from "./scanner.js";
|
|
5
|
+
import { buildSshSessionAction } from "./ssh.js";
|
|
6
|
+
export async function scanLocalText(store, text, options = {}) {
|
|
7
|
+
const persist = options.persist === true;
|
|
8
|
+
const backend = options.backend || "local";
|
|
9
|
+
return scanText(text, async (candidate) => {
|
|
10
|
+
if (!persist) {
|
|
11
|
+
return previewHandle(candidate);
|
|
12
|
+
}
|
|
13
|
+
const record = await addLocalSecret(store, {
|
|
14
|
+
name: nameForCandidate(candidate, options.defaultName),
|
|
15
|
+
type: candidate.type,
|
|
16
|
+
provider: candidate.provider,
|
|
17
|
+
ruleId: candidate.ruleId,
|
|
18
|
+
severity: candidate.severity,
|
|
19
|
+
confidence: candidate.confidence,
|
|
20
|
+
value: candidate.value,
|
|
21
|
+
source: options.source,
|
|
22
|
+
policy: {
|
|
23
|
+
allowedCommands: [],
|
|
24
|
+
maxOutputBytes: 16_384
|
|
25
|
+
}
|
|
26
|
+
}, backend);
|
|
27
|
+
return record.handle;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function addLocalSecret(store, input, backend = "local") {
|
|
31
|
+
if (backend === "keychain") {
|
|
32
|
+
return store.addKeychainSecret(input);
|
|
33
|
+
}
|
|
34
|
+
return store.addSecret(input);
|
|
35
|
+
}
|
|
36
|
+
export function preferredLocalSecretBackend() {
|
|
37
|
+
const configured = process.env.SGW_SECRET_BACKEND?.trim().toLowerCase();
|
|
38
|
+
if (configured === "local" || configured === "keychain") {
|
|
39
|
+
return configured;
|
|
40
|
+
}
|
|
41
|
+
if ((process.platform === "darwin" || process.platform === "win32") && process.env.SGW_DISABLE_KEYCHAIN !== "1") {
|
|
42
|
+
return "keychain";
|
|
43
|
+
}
|
|
44
|
+
return "local";
|
|
45
|
+
}
|
|
46
|
+
export async function scanTextToOnePassword(store, text, options = {}) {
|
|
47
|
+
const vault = options.vault || "Dev";
|
|
48
|
+
return scanText(text, async (candidate) => {
|
|
49
|
+
const title = onePasswordTitleForCandidate(candidate, options.defaultName);
|
|
50
|
+
const created = await createOnePasswordSecretReference({
|
|
51
|
+
vault,
|
|
52
|
+
title,
|
|
53
|
+
type: candidate.type,
|
|
54
|
+
value: candidate.value,
|
|
55
|
+
notes: onePasswordNotes(options.source)
|
|
56
|
+
});
|
|
57
|
+
const record = await store.addOnePasswordReference({
|
|
58
|
+
name: title,
|
|
59
|
+
type: candidate.type,
|
|
60
|
+
reference: created.reference,
|
|
61
|
+
source: `onepassword:${vault}`,
|
|
62
|
+
policy: options.policy || {
|
|
63
|
+
allowedCommands: [],
|
|
64
|
+
maxOutputBytes: 16_384
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return record.handle;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export async function scanLocalFile(store, filePath, options = {}) {
|
|
71
|
+
const resolved = path.resolve(filePath);
|
|
72
|
+
const text = await readFile(resolved, "utf8");
|
|
73
|
+
return scanLocalText(store, text, {
|
|
74
|
+
persist: options.persist ?? true,
|
|
75
|
+
source: resolved,
|
|
76
|
+
defaultName: options.defaultName || path.basename(resolved),
|
|
77
|
+
backend: options.backend
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function onePasswordTitleForCandidate(candidate, defaultName) {
|
|
81
|
+
const base = defaultName?.trim() || `s-gw captured ${candidate.label || friendlyType(candidate.type)}`;
|
|
82
|
+
const stamp = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 15);
|
|
83
|
+
return `${base} ${stamp}-${candidate.start}`;
|
|
84
|
+
}
|
|
85
|
+
function onePasswordNotes(source) {
|
|
86
|
+
const suffix = source ? ` Source: ${source}` : "";
|
|
87
|
+
return `Created by s-gw from a credential capture.${suffix}`;
|
|
88
|
+
}
|
|
89
|
+
export function buildEnvCommandAction(input) {
|
|
90
|
+
return {
|
|
91
|
+
kind: "env_command",
|
|
92
|
+
command: input.command,
|
|
93
|
+
args: input.args || [],
|
|
94
|
+
injectEnv: input.injectEnv,
|
|
95
|
+
env: input.env,
|
|
96
|
+
workingDir: input.workingDir,
|
|
97
|
+
timeoutMs: input.timeoutMs ?? 30_000
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function nameForCandidate(candidate, defaultName) {
|
|
101
|
+
const prefix = defaultName ? `${defaultName} ` : "";
|
|
102
|
+
return `${prefix}${candidate.label || friendlyType(candidate.type)} ${candidate.start}`;
|
|
103
|
+
}
|
|
104
|
+
function friendlyType(type) {
|
|
105
|
+
if (type === "api-token") {
|
|
106
|
+
return "API token";
|
|
107
|
+
}
|
|
108
|
+
if (type === "private-key") {
|
|
109
|
+
return "private key";
|
|
110
|
+
}
|
|
111
|
+
return type;
|
|
112
|
+
}
|
|
113
|
+
export { buildSshSessionAction };
|
|
114
|
+
//# sourceMappingURL=gateway.js.map
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { SecretStore } from "./store.js";
|
|
2
|
+
export interface GuardRunOptions {
|
|
3
|
+
agent: string;
|
|
4
|
+
command?: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
extraEnv?: Record<string, string>;
|
|
9
|
+
scrubEnv?: boolean;
|
|
10
|
+
persist?: boolean;
|
|
11
|
+
allowedCommands?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface GuardEnvFinding {
|
|
14
|
+
name: string;
|
|
15
|
+
handle: string;
|
|
16
|
+
token: string;
|
|
17
|
+
type: string;
|
|
18
|
+
provider?: string;
|
|
19
|
+
ruleId?: string;
|
|
20
|
+
severity?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GuardRunPlan {
|
|
23
|
+
agent: {
|
|
24
|
+
id: string;
|
|
25
|
+
displayName: string;
|
|
26
|
+
};
|
|
27
|
+
command: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
cwd: string;
|
|
30
|
+
mode: "guarded";
|
|
31
|
+
scrubbedEnv: GuardEnvFinding[];
|
|
32
|
+
keptEnvCount: number;
|
|
33
|
+
scrubbedEnvCount: number;
|
|
34
|
+
mcp: {
|
|
35
|
+
serverName: string;
|
|
36
|
+
command: string;
|
|
37
|
+
};
|
|
38
|
+
instructions: string;
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface GuardRunPreparation {
|
|
43
|
+
plan: GuardRunPlan;
|
|
44
|
+
env: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
export declare function guardStatus(): {
|
|
47
|
+
mode: string;
|
|
48
|
+
envScrubbing: boolean;
|
|
49
|
+
rawSecretPolicy: string;
|
|
50
|
+
configInstall: string;
|
|
51
|
+
agents: {
|
|
52
|
+
id: string;
|
|
53
|
+
displayName: string;
|
|
54
|
+
mcpStatus: import("./agents.js").AgentMcpStatus;
|
|
55
|
+
configPaths: string[];
|
|
56
|
+
defaultRunCommand: string;
|
|
57
|
+
directRunSupported: boolean;
|
|
58
|
+
}[];
|
|
59
|
+
};
|
|
60
|
+
export declare function prepareGuardedRun(store: SecretStore, options: GuardRunOptions): Promise<GuardRunPreparation>;
|
|
61
|
+
export declare function runGuardedAgent(store: SecretStore, options: GuardRunOptions): Promise<number>;
|