apex-code 0.0.6 → 0.1.1
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/CHANGELOG.md +33 -0
- package/README.md +8 -9
- package/dist/cli/args.d.ts +0 -12
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -32
- package/dist/cli/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -130
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +0 -8
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/delegation/agents.d.ts.map +1 -1
- package/dist/core/delegation/agents.js +2 -1
- package/dist/core/delegation/agents.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +6 -14
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +1 -2
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +3 -4
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +4 -5
- package/dist/core/extensions/source-runtime.d.ts.map +1 -1
- package/dist/core/extensions/source-runtime.js +4 -5
- package/dist/core/extensions/source-runtime.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +2 -1
- package/dist/core/formatter-lifecycle.d.ts.map +1 -1
- package/dist/core/formatter-lifecycle.js +2 -1
- package/dist/core/formatter-lifecycle.js.map +1 -1
- package/dist/core/hooks/types.d.ts +2 -3
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js +2 -3
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/http-idle-timeout.d.ts +3 -5
- package/dist/core/http-idle-timeout.d.ts.map +1 -1
- package/dist/core/http-idle-timeout.js +3 -5
- package/dist/core/http-idle-timeout.js.map +1 -1
- package/dist/core/mcp/config.d.ts +18 -1
- package/dist/core/mcp/config.d.ts.map +1 -1
- package/dist/core/mcp/config.js +24 -7
- package/dist/core/mcp/config.js.map +1 -1
- package/dist/core/mcp/connector.d.ts +2 -3
- package/dist/core/mcp/connector.d.ts.map +1 -1
- package/dist/core/mcp/connector.js.map +1 -1
- package/dist/core/mcp/oauth/authorize.d.ts +2 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
- package/dist/core/mcp/oauth/authorize.js +5 -2
- package/dist/core/mcp/oauth/authorize.js.map +1 -1
- package/dist/core/mcp/oauth/mcp-token.d.ts +4 -6
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -1
- package/dist/core/mcp/oauth/mcp-token.js +5 -7
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -1
- package/dist/core/mcp/runtime.d.ts +1 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -1
- package/dist/core/mcp/runtime.js +2 -1
- package/dist/core/mcp/runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +0 -15
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/store.d.ts +6 -11
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +23 -51
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/project-resources.d.ts +62 -0
- package/dist/core/project-resources.d.ts.map +1 -0
- package/dist/core/project-resources.js +42 -0
- package/dist/core/project-resources.js.map +1 -0
- package/dist/core/sdk.d.ts +1 -9
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +10 -35
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-share.d.ts +6 -8
- package/dist/core/session-share.d.ts.map +1 -1
- package/dist/core/session-share.js +9 -12
- package/dist/core/session-share.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -21
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -12
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +0 -47
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/web-fetch.d.ts.map +1 -1
- package/dist/core/tools/web-fetch.js +4 -8
- package/dist/core/tools/web-fetch.js.map +1 -1
- package/dist/core/tools/web-search-exa.d.ts +3 -6
- package/dist/core/tools/web-search-exa.d.ts.map +1 -1
- package/dist/core/tools/web-search-exa.js +5 -8
- package/dist/core/tools/web-search-exa.js.map +1 -1
- package/dist/core/trust-manager.d.ts +6 -5
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +42 -19
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/web-search-provider.d.ts +0 -10
- package/dist/core/web-search-provider.d.ts.map +1 -1
- package/dist/core/web-search-provider.js +2 -16
- package/dist/core/web-search-provider.js.map +1 -1
- package/dist/main.d.ts +0 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +0 -29
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +0 -25
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +3 -52
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +0 -2
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +0 -2
- package/dist/utils/version-check.js.map +1 -1
- package/docs/containerization.md +8 -4
- package/docs/sdk.md +17 -24
- package/docs/security.md +7 -5
- package/docs/settings.md +2 -7
- package/docs/skills.md +4 -7
- package/npm-shrinkwrap.json +5 -5
- package/package.json +2 -2
- package/dist/core/sandbox/bwrap-arguments.d.ts +0 -41
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +0 -1
- package/dist/core/sandbox/bwrap-arguments.js +0 -136
- package/dist/core/sandbox/bwrap-arguments.js.map +0 -1
- package/dist/core/sandbox/child-entry.d.ts +0 -2
- package/dist/core/sandbox/child-entry.d.ts.map +0 -1
- package/dist/core/sandbox/child-entry.js +0 -24
- package/dist/core/sandbox/child-entry.js.map +0 -1
- package/dist/core/sandbox/cli-launch.d.ts +0 -162
- package/dist/core/sandbox/cli-launch.d.ts.map +0 -1
- package/dist/core/sandbox/cli-launch.js +0 -366
- package/dist/core/sandbox/cli-launch.js.map +0 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +0 -31
- package/dist/core/sandbox/cli-supervisor.d.ts.map +0 -1
- package/dist/core/sandbox/cli-supervisor.js +0 -147
- package/dist/core/sandbox/cli-supervisor.js.map +0 -1
- package/dist/core/sandbox/default-hosts.d.ts +0 -28
- package/dist/core/sandbox/default-hosts.d.ts.map +0 -1
- package/dist/core/sandbox/default-hosts.js +0 -69
- package/dist/core/sandbox/default-hosts.js.map +0 -1
- package/dist/core/sandbox/full-access.d.ts +0 -28
- package/dist/core/sandbox/full-access.d.ts.map +0 -1
- package/dist/core/sandbox/full-access.js +0 -63
- package/dist/core/sandbox/full-access.js.map +0 -1
- package/dist/core/sandbox/git-identity.d.ts +0 -57
- package/dist/core/sandbox/git-identity.d.ts.map +0 -1
- package/dist/core/sandbox/git-identity.js +0 -78
- package/dist/core/sandbox/git-identity.js.map +0 -1
- package/dist/core/sandbox/host-approval.d.ts +0 -42
- package/dist/core/sandbox/host-approval.d.ts.map +0 -1
- package/dist/core/sandbox/host-approval.js +0 -99
- package/dist/core/sandbox/host-approval.js.map +0 -1
- package/dist/core/sandbox/linux-backend.d.ts +0 -48
- package/dist/core/sandbox/linux-backend.d.ts.map +0 -1
- package/dist/core/sandbox/linux-backend.js +0 -328
- package/dist/core/sandbox/linux-backend.js.map +0 -1
- package/dist/core/sandbox/macos-backend.d.ts +0 -30
- package/dist/core/sandbox/macos-backend.d.ts.map +0 -1
- package/dist/core/sandbox/macos-backend.js +0 -385
- package/dist/core/sandbox/macos-backend.js.map +0 -1
- package/dist/core/sandbox/network-proxy.d.ts +0 -38
- package/dist/core/sandbox/network-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/network-proxy.js +0 -153
- package/dist/core/sandbox/network-proxy.js.map +0 -1
- package/dist/core/sandbox/network-refusal.d.ts +0 -24
- package/dist/core/sandbox/network-refusal.d.ts.map +0 -1
- package/dist/core/sandbox/network-refusal.js +0 -72
- package/dist/core/sandbox/network-refusal.js.map +0 -1
- package/dist/core/sandbox/policy.d.ts +0 -40
- package/dist/core/sandbox/policy.d.ts.map +0 -1
- package/dist/core/sandbox/policy.js +0 -55
- package/dist/core/sandbox/policy.js.map +0 -1
- package/dist/core/sandbox/profiles.d.ts +0 -25
- package/dist/core/sandbox/profiles.d.ts.map +0 -1
- package/dist/core/sandbox/profiles.js +0 -23
- package/dist/core/sandbox/profiles.js.map +0 -1
- package/dist/core/sandbox/rpc/command-client.d.ts +0 -28
- package/dist/core/sandbox/rpc/command-client.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/command-client.js +0 -86
- package/dist/core/sandbox/rpc/command-client.js.map +0 -1
- package/dist/core/sandbox/rpc/command-proxy.d.ts +0 -48
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/command-proxy.js +0 -120
- package/dist/core/sandbox/rpc/command-proxy.js.map +0 -1
- package/dist/core/sandbox/rpc/credential-client.d.ts +0 -20
- package/dist/core/sandbox/rpc/credential-client.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/credential-client.js +0 -170
- package/dist/core/sandbox/rpc/credential-client.js.map +0 -1
- package/dist/core/sandbox/rpc/credential-proxy.d.ts +0 -44
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +0 -287
- package/dist/core/sandbox/rpc/credential-proxy.js.map +0 -1
- package/dist/core/sandbox/rpc/framing.d.ts +0 -35
- package/dist/core/sandbox/rpc/framing.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/framing.js +0 -114
- package/dist/core/sandbox/rpc/framing.js.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +0 -47
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-helper.js +0 -222
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +0 -66
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +0 -1
- package/dist/core/sandbox/rpc/git-credential-proxy.js +0 -209
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +0 -1
- package/dist/core/sandbox/supervisor-temp.d.ts +0 -18
- package/dist/core/sandbox/supervisor-temp.d.ts.map +0 -1
- package/dist/core/sandbox/supervisor-temp.js +0 -21
- package/dist/core/sandbox/supervisor-temp.js.map +0 -1
- package/dist/core/sandbox/supervisor.d.ts +0 -62
- package/dist/core/sandbox/supervisor.d.ts.map +0 -1
- package/dist/core/sandbox/supervisor.js +0 -32
- package/dist/core/sandbox/supervisor.js.map +0 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +0 -68
- package/dist/core/sandbox/terminal-handoff.d.ts.map +0 -1
- package/dist/core/sandbox/terminal-handoff.js +0 -221
- package/dist/core/sandbox/terminal-handoff.js.map +0 -1
- package/dist/core/sandbox/terminal-size.d.ts +0 -53
- package/dist/core/sandbox/terminal-size.d.ts.map +0 -1
- package/dist/core/sandbox/terminal-size.js +0 -126
- package/dist/core/sandbox/terminal-size.js.map +0 -1
- package/dist/core/sandbox/violations.d.ts +0 -23
- package/dist/core/sandbox/violations.d.ts.map +0 -1
- package/dist/core/sandbox/violations.js +0 -30
- package/dist/core/sandbox/violations.js.map +0 -1
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supervisor-owned credential mutation service for sandboxed sessions.
|
|
3
|
-
*
|
|
4
|
-
* Reads stay on the read-only auth.json projection. Mutations cross this Unix socket
|
|
5
|
-
* and execute under the host AuthStorage lock. The child can therefore implement the
|
|
6
|
-
* CredentialStore serialized read-modify-write contract without gaining filesystem
|
|
7
|
-
* write access to the credential file.
|
|
8
|
-
*/
|
|
9
|
-
import { chmodSync, lstatSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
10
|
-
import * as net from "node:net";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { AuthStorage } from "../../auth-storage.js";
|
|
13
|
-
import { getConfigValueEnvVarNames, isCommandConfigValue } from "../../resolve-config-value.js";
|
|
14
|
-
import { FrameReader, isRequestObject, writeFrame } from "./framing.js";
|
|
15
|
-
/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
|
|
16
|
-
const SUN_PATH_LIMIT = 108;
|
|
17
|
-
/** Credentials are small; reject larger frames before UTF-8 decoding or JSON parsing. */
|
|
18
|
-
const MAX_CONNECTIONS = 32;
|
|
19
|
-
const CONNECTION_IDLE_TIMEOUT_MS = 30_000;
|
|
20
|
-
function processIsAlive(pid) {
|
|
21
|
-
try {
|
|
22
|
-
process.kill(pid, 0);
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
return error.code === "EPERM";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/** Reclaim only same-user directories whose encoded supervisor PID is dead. */
|
|
30
|
-
function reclaimStaleCredentialChannelDirectories() {
|
|
31
|
-
let entries;
|
|
32
|
-
try {
|
|
33
|
-
entries = readdirSync("/tmp");
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
const match = /^apex-cred-(\d+)-[A-Za-z0-9_-]+$/.exec(entry);
|
|
40
|
-
if (!match)
|
|
41
|
-
continue;
|
|
42
|
-
const pid = Number(match[1]);
|
|
43
|
-
if (!Number.isSafeInteger(pid) || pid < 1 || processIsAlive(pid))
|
|
44
|
-
continue;
|
|
45
|
-
const path = join("/tmp", entry);
|
|
46
|
-
try {
|
|
47
|
-
const stat = lstatSync(path);
|
|
48
|
-
if (!stat.isDirectory() || stat.uid !== process.getuid?.())
|
|
49
|
-
continue;
|
|
50
|
-
rmSync(path, { force: true, recursive: true });
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// Another launch may have reclaimed it first.
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Allocate the endpoint beneath a private fixed-root directory.
|
|
59
|
-
*
|
|
60
|
-
* Do not honor TMPDIR here. It can point inside the workspace on macOS, where the
|
|
61
|
-
* sandbox may write it. A fresh 0700 directory under /tmp also removes the
|
|
62
|
-
* listen-before-chmod authorization race: no other account can reach the socket while
|
|
63
|
-
* Node applies its initial umask-derived mode.
|
|
64
|
-
*/
|
|
65
|
-
export function resolveCredentialChannelPaths() {
|
|
66
|
-
reclaimStaleCredentialChannelDirectories();
|
|
67
|
-
const hostSocketDirectory = mkdtempSync(`/tmp/apex-cred-${process.pid}-`, { encoding: "utf8" });
|
|
68
|
-
chmodSync(hostSocketDirectory, 0o700);
|
|
69
|
-
const hostSocketPath = join(hostSocketDirectory, "channel.sock");
|
|
70
|
-
if (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {
|
|
71
|
-
rmSync(hostSocketDirectory, { force: true, recursive: true });
|
|
72
|
-
throw new Error("Credential channel path exceeds the Unix socket path limit.");
|
|
73
|
-
}
|
|
74
|
-
const childSocketPath = process.platform === "linux" ? "/home/apex-credential-channel.sock" : hostSocketPath;
|
|
75
|
-
return { hostSocketDirectory, hostSocketPath, childSocketPath };
|
|
76
|
-
}
|
|
77
|
-
/** Find a config-value reference without recursively walking attacker-controlled input. */
|
|
78
|
-
export function findCredentialConfigReference(credential) {
|
|
79
|
-
const pending = [{ value: credential, field: "" }];
|
|
80
|
-
while (pending.length > 0) {
|
|
81
|
-
const current = pending.pop();
|
|
82
|
-
if (!current)
|
|
83
|
-
break;
|
|
84
|
-
if (typeof current.value === "string") {
|
|
85
|
-
if (isCommandConfigValue(current.value))
|
|
86
|
-
return { field: current.field, kind: "command" };
|
|
87
|
-
if (getConfigValueEnvVarNames(current.value).length > 0) {
|
|
88
|
-
return { field: current.field, kind: "environment" };
|
|
89
|
-
}
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (Array.isArray(current.value)) {
|
|
93
|
-
for (let index = current.value.length - 1; index >= 0; index--) {
|
|
94
|
-
pending.push({ value: current.value[index], field: `${current.field}[${index}]` });
|
|
95
|
-
}
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (current.value !== null && typeof current.value === "object") {
|
|
99
|
-
const entries = Object.entries(current.value);
|
|
100
|
-
for (let index = entries.length - 1; index >= 0; index--) {
|
|
101
|
-
const [key, value] = entries[index];
|
|
102
|
-
pending.push({ value, field: current.field ? `${current.field}.${key}` : key });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
function isStringRecord(value) {
|
|
109
|
-
return (typeof value === "object" &&
|
|
110
|
-
value !== null &&
|
|
111
|
-
!Array.isArray(value) &&
|
|
112
|
-
Object.values(value).every((entry) => typeof entry === "string"));
|
|
113
|
-
}
|
|
114
|
-
/** Runtime validation at the untrusted child-to-host boundary. */
|
|
115
|
-
export function isCredential(value) {
|
|
116
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
117
|
-
return false;
|
|
118
|
-
const credential = value;
|
|
119
|
-
if (credential.type === "api_key") {
|
|
120
|
-
return ((credential.key === undefined || typeof credential.key === "string") &&
|
|
121
|
-
(credential.env === undefined || isStringRecord(credential.env)));
|
|
122
|
-
}
|
|
123
|
-
return (credential.type === "oauth" &&
|
|
124
|
-
typeof credential.access === "string" &&
|
|
125
|
-
typeof credential.refresh === "string" &&
|
|
126
|
-
typeof credential.expires === "number" &&
|
|
127
|
-
Number.isFinite(credential.expires));
|
|
128
|
-
}
|
|
129
|
-
function referenceRefusal(reference) {
|
|
130
|
-
return (`Credential values written from a sandboxed session must be literal secrets: field ` +
|
|
131
|
-
`"${reference.field}" contains a ${reference.kind} reference. To store a reference, ` +
|
|
132
|
-
`edit the credential file on the host directly or use the provider environment variable.`);
|
|
133
|
-
}
|
|
134
|
-
class ChannelRefusal extends Error {
|
|
135
|
-
command;
|
|
136
|
-
constructor(command, message) {
|
|
137
|
-
super(message);
|
|
138
|
-
this.name = "ChannelRefusal";
|
|
139
|
-
this.command = command;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/** Serve serialized credential mutations on one private Unix socket. */
|
|
143
|
-
export function createCredentialProxy(options) {
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
const authStorage = AuthStorage.create(options.authPath);
|
|
146
|
-
const sockets = new Set();
|
|
147
|
-
const operations = new Set();
|
|
148
|
-
let settled = false;
|
|
149
|
-
let closing;
|
|
150
|
-
const audit = (command, detail) => {
|
|
151
|
-
options.violationStore?.add({ kind: "unknown", command, detail, timestamp: new Date() });
|
|
152
|
-
};
|
|
153
|
-
const refuse = (socket, command, error, detail = error) => {
|
|
154
|
-
audit(command, `Refused: ${detail}`);
|
|
155
|
-
writeFrame(socket, { ok: false, error });
|
|
156
|
-
};
|
|
157
|
-
const handleConnection = async (socket, reader, signal) => {
|
|
158
|
-
const raw = await reader.next();
|
|
159
|
-
if (!isRequestObject(raw)) {
|
|
160
|
-
refuse(socket, "credential-channel", "Invalid request: expected an object.");
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const request = raw;
|
|
164
|
-
const providerId = request.providerId;
|
|
165
|
-
if (typeof providerId !== "string" ||
|
|
166
|
-
providerId.length === 0 ||
|
|
167
|
-
Buffer.byteLength(providerId, "utf8") > 256 ||
|
|
168
|
-
/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(providerId)) {
|
|
169
|
-
refuse(socket, "credential-channel", "Invalid request: providerId contains unsafe characters or is too long.");
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (request.action === "delete") {
|
|
173
|
-
await authStorage.delete(providerId, { signal });
|
|
174
|
-
audit(`credential-delete ${providerId}`, "Accepted: credential deleted through the sandbox channel.");
|
|
175
|
-
writeFrame(socket, { ok: true });
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (request.action !== "modify") {
|
|
179
|
-
refuse(socket, "credential-channel", 'Invalid request: action must be "modify" or "delete".');
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
let changed = false;
|
|
183
|
-
const result = await authStorage.modify(providerId, async (current) => {
|
|
184
|
-
writeFrame(socket, { type: "current", ...(current === undefined ? {} : { credential: current }) });
|
|
185
|
-
const rawCommit = await reader.next();
|
|
186
|
-
if (!isRequestObject(rawCommit)) {
|
|
187
|
-
throw new ChannelRefusal(`credential-write ${providerId}`, "Invalid modify response: expected an object.");
|
|
188
|
-
}
|
|
189
|
-
const commit = rawCommit;
|
|
190
|
-
if (commit.action === "abort") {
|
|
191
|
-
throw new ChannelRefusal(`credential-write ${providerId}`, "Credential modification was aborted by the client.");
|
|
192
|
-
}
|
|
193
|
-
if (commit.action === "no_change")
|
|
194
|
-
return undefined;
|
|
195
|
-
if (commit.action !== "commit") {
|
|
196
|
-
throw new ChannelRefusal(`credential-write ${providerId}`, "Invalid modify response action.");
|
|
197
|
-
}
|
|
198
|
-
if (!isCredential(commit.credential)) {
|
|
199
|
-
throw new ChannelRefusal(`credential-write ${providerId}`, "Invalid credential shape.");
|
|
200
|
-
}
|
|
201
|
-
const reference = findCredentialConfigReference(commit.credential);
|
|
202
|
-
if (reference) {
|
|
203
|
-
throw new ChannelRefusal(`credential-write ${providerId}`, referenceRefusal(reference));
|
|
204
|
-
}
|
|
205
|
-
changed = true;
|
|
206
|
-
return commit.credential;
|
|
207
|
-
}, { signal });
|
|
208
|
-
if (changed) {
|
|
209
|
-
audit(`credential-write ${providerId}`, "Accepted: credential written through the sandbox channel.");
|
|
210
|
-
}
|
|
211
|
-
writeFrame(socket, { ok: true, ...(result === undefined ? {} : { credential: result }) });
|
|
212
|
-
};
|
|
213
|
-
const server = net.createServer((socket) => {
|
|
214
|
-
if (sockets.size >= MAX_CONNECTIONS) {
|
|
215
|
-
refuse(socket, "credential-channel", "Credential channel connection limit reached.");
|
|
216
|
-
socket.end();
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
sockets.add(socket);
|
|
220
|
-
const controller = new AbortController();
|
|
221
|
-
socket.setTimeout(CONNECTION_IDLE_TIMEOUT_MS, () => {
|
|
222
|
-
audit("credential-channel", "Refused: credential channel connection timed out.");
|
|
223
|
-
socket.destroy(new Error("Credential channel connection timed out."));
|
|
224
|
-
});
|
|
225
|
-
socket.on("close", () => {
|
|
226
|
-
sockets.delete(socket);
|
|
227
|
-
controller.abort();
|
|
228
|
-
});
|
|
229
|
-
const reader = new FrameReader(socket, "Credential channel", (detail) => audit("credential-channel", `Refused: ${detail}`));
|
|
230
|
-
const operation = handleConnection(socket, reader, controller.signal)
|
|
231
|
-
.catch((error) => {
|
|
232
|
-
if (error instanceof ChannelRefusal) {
|
|
233
|
-
refuse(socket, error.command, error.message);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
refuse(socket, "credential-channel", "The host credential operation failed.", "Host credential operation failed without exposing credential data.");
|
|
237
|
-
})
|
|
238
|
-
.finally(() => {
|
|
239
|
-
operations.delete(operation);
|
|
240
|
-
socket.end();
|
|
241
|
-
});
|
|
242
|
-
operations.add(operation);
|
|
243
|
-
});
|
|
244
|
-
const cleanup = () => {
|
|
245
|
-
rmSync(options.socketPath, { force: true });
|
|
246
|
-
if (options.cleanupDirectory)
|
|
247
|
-
rmSync(options.cleanupDirectory, { force: true, recursive: true });
|
|
248
|
-
};
|
|
249
|
-
const failStartup = (error) => {
|
|
250
|
-
if (settled)
|
|
251
|
-
return;
|
|
252
|
-
settled = true;
|
|
253
|
-
server.close();
|
|
254
|
-
for (const socket of sockets)
|
|
255
|
-
socket.destroy();
|
|
256
|
-
cleanup();
|
|
257
|
-
reject(error);
|
|
258
|
-
};
|
|
259
|
-
server.once("error", failStartup);
|
|
260
|
-
server.listen(options.socketPath, () => {
|
|
261
|
-
try {
|
|
262
|
-
chmodSync(options.socketPath, 0o600);
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
failStartup(error instanceof Error ? error : new Error(String(error)));
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
settled = true;
|
|
269
|
-
server.off("error", failStartup);
|
|
270
|
-
resolve({
|
|
271
|
-
close: () => {
|
|
272
|
-
if (closing)
|
|
273
|
-
return closing;
|
|
274
|
-
closing = (async () => {
|
|
275
|
-
for (const socket of sockets)
|
|
276
|
-
socket.destroy();
|
|
277
|
-
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
278
|
-
await Promise.allSettled([...operations]);
|
|
279
|
-
cleanup();
|
|
280
|
-
})();
|
|
281
|
-
return closing;
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
//# sourceMappingURL=credential-proxy.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"credential-proxy.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/credential-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Supervisor-owned credential mutation service for sandboxed sessions.\n *\n * Reads stay on the read-only auth.json projection. Mutations cross this Unix socket\n * and execute under the host AuthStorage lock. The child can therefore implement the\n * CredentialStore serialized read-modify-write contract without gaining filesystem\n * write access to the credential file.\n */\n\nimport { chmodSync, lstatSync, mkdtempSync, readdirSync, rmSync } from \"node:fs\";\nimport * as net from \"node:net\";\nimport { join } from \"node:path\";\nimport type { Credential } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue } from \"../../resolve-config-value.ts\";\nimport type { SandboxViolationStore } from \"../violations.ts\";\nimport { FrameReader, isRequestObject, writeFrame } from \"./framing.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n/** Credentials are small; reject larger frames before UTF-8 decoding or JSON parsing. */\nconst MAX_CONNECTIONS = 32;\nconst CONNECTION_IDLE_TIMEOUT_MS = 30_000;\n\nfunction processIsAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\n/** Reclaim only same-user directories whose encoded supervisor PID is dead. */\nfunction reclaimStaleCredentialChannelDirectories(): void {\n\tlet entries: string[];\n\ttry {\n\t\tentries = readdirSync(\"/tmp\");\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const entry of entries) {\n\t\tconst match = /^apex-cred-(\\d+)-[A-Za-z0-9_-]+$/.exec(entry);\n\t\tif (!match) continue;\n\t\tconst pid = Number(match[1]);\n\t\tif (!Number.isSafeInteger(pid) || pid < 1 || processIsAlive(pid)) continue;\n\t\tconst path = join(\"/tmp\", entry);\n\t\ttry {\n\t\t\tconst stat = lstatSync(path);\n\t\t\tif (!stat.isDirectory() || stat.uid !== process.getuid?.()) continue;\n\t\t\trmSync(path, { force: true, recursive: true });\n\t\t} catch {\n\t\t\t// Another launch may have reclaimed it first.\n\t\t}\n\t}\n}\n\nexport interface CredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Allocate the endpoint beneath a private fixed-root directory.\n *\n * Do not honor TMPDIR here. It can point inside the workspace on macOS, where the\n * sandbox may write it. A fresh 0700 directory under /tmp also removes the\n * listen-before-chmod authorization race: no other account can reach the socket while\n * Node applies its initial umask-derived mode.\n */\nexport function resolveCredentialChannelPaths(): CredentialChannelPaths {\n\treclaimStaleCredentialChannelDirectories();\n\tconst hostSocketDirectory = mkdtempSync(`/tmp/apex-cred-${process.pid}-`, { encoding: \"utf8\" });\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\trmSync(hostSocketDirectory, { force: true, recursive: true });\n\t\tthrow new Error(\"Credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\nexport interface CredentialProxy {\n\tclose(): Promise<void>;\n}\n\nexport interface CredentialConfigReference {\n\treadonly field: string;\n\treadonly kind: \"command\" | \"environment\";\n}\n\n/** Find a config-value reference without recursively walking attacker-controlled input. */\nexport function findCredentialConfigReference(credential: unknown): CredentialConfigReference | undefined {\n\tconst pending: Array<{ value: unknown; field: string }> = [{ value: credential, field: \"\" }];\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop();\n\t\tif (!current) break;\n\t\tif (typeof current.value === \"string\") {\n\t\t\tif (isCommandConfigValue(current.value)) return { field: current.field, kind: \"command\" };\n\t\t\tif (getConfigValueEnvVarNames(current.value).length > 0) {\n\t\t\t\treturn { field: current.field, kind: \"environment\" };\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(current.value)) {\n\t\t\tfor (let index = current.value.length - 1; index >= 0; index--) {\n\t\t\t\tpending.push({ value: current.value[index], field: `${current.field}[${index}]` });\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (current.value !== null && typeof current.value === \"object\") {\n\t\t\tconst entries = Object.entries(current.value);\n\t\t\tfor (let index = entries.length - 1; index >= 0; index--) {\n\t\t\t\tconst [key, value] = entries[index]!;\n\t\t\t\tpending.push({ value, field: current.field ? `${current.field}.${key}` : key });\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t!Array.isArray(value) &&\n\t\tObject.values(value).every((entry) => typeof entry === \"string\")\n\t);\n}\n\n/** Runtime validation at the untrusted child-to-host boundary. */\nexport function isCredential(value: unknown): value is Credential {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst credential = value as Record<string, unknown>;\n\tif (credential.type === \"api_key\") {\n\t\treturn (\n\t\t\t(credential.key === undefined || typeof credential.key === \"string\") &&\n\t\t\t(credential.env === undefined || isStringRecord(credential.env))\n\t\t);\n\t}\n\treturn (\n\t\tcredential.type === \"oauth\" &&\n\t\ttypeof credential.access === \"string\" &&\n\t\ttypeof credential.refresh === \"string\" &&\n\t\ttypeof credential.expires === \"number\" &&\n\t\tNumber.isFinite(credential.expires)\n\t);\n}\n\nfunction referenceRefusal(reference: CredentialConfigReference): string {\n\treturn (\n\t\t`Credential values written from a sandboxed session must be literal secrets: field ` +\n\t\t`\"${reference.field}\" contains a ${reference.kind} reference. To store a reference, ` +\n\t\t`edit the credential file on the host directly or use the provider environment variable.`\n\t);\n}\n\ninterface StartRequest {\n\treadonly action?: unknown;\n\treadonly providerId?: unknown;\n}\n\ninterface CommitRequest {\n\treadonly action?: unknown;\n\treadonly credential?: unknown;\n}\n\nclass ChannelRefusal extends Error {\n\treadonly command: string;\n\n\tconstructor(command: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"ChannelRefusal\";\n\t\tthis.command = command;\n\t}\n}\n\n/** Serve serialized credential mutations on one private Unix socket. */\nexport function createCredentialProxy(options: {\n\tauthPath: string;\n\tviolationStore?: SandboxViolationStore;\n\tsocketPath: string;\n\t/** Private directory allocated by resolveCredentialChannelPaths; removed on close. */\n\tcleanupDirectory?: string;\n}): Promise<CredentialProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst authStorage = AuthStorage.create(options.authPath);\n\t\tconst sockets = new Set<net.Socket>();\n\t\tconst operations = new Set<Promise<void>>();\n\t\tlet settled = false;\n\t\tlet closing: Promise<void> | undefined;\n\n\t\tconst audit = (command: string, detail: string): void => {\n\t\t\toptions.violationStore?.add({ kind: \"unknown\", command, detail, timestamp: new Date() });\n\t\t};\n\t\tconst refuse = (socket: net.Socket, command: string, error: string, detail = error): void => {\n\t\t\taudit(command, `Refused: ${detail}`);\n\t\t\twriteFrame(socket, { ok: false, error });\n\t\t};\n\n\t\tconst handleConnection = async (socket: net.Socket, reader: FrameReader, signal: AbortSignal): Promise<void> => {\n\t\t\tconst raw = await reader.next();\n\t\t\tif (!isRequestObject(raw)) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Invalid request: expected an object.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst request = raw as StartRequest;\n\t\t\tconst providerId = request.providerId;\n\t\t\tif (\n\t\t\t\ttypeof providerId !== \"string\" ||\n\t\t\t\tproviderId.length === 0 ||\n\t\t\t\tBuffer.byteLength(providerId, \"utf8\") > 256 ||\n\t\t\t\t/[\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]/u.test(providerId)\n\t\t\t) {\n\t\t\t\trefuse(\n\t\t\t\t\tsocket,\n\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\"Invalid request: providerId contains unsafe characters or is too long.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action === \"delete\") {\n\t\t\t\tawait authStorage.delete(providerId, { signal });\n\t\t\t\taudit(`credential-delete ${providerId}`, \"Accepted: credential deleted through the sandbox channel.\");\n\t\t\t\twriteFrame(socket, { ok: true });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (request.action !== \"modify\") {\n\t\t\t\trefuse(socket, \"credential-channel\", 'Invalid request: action must be \"modify\" or \"delete\".');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet changed = false;\n\t\t\tconst result = await authStorage.modify(\n\t\t\t\tproviderId,\n\t\t\t\tasync (current) => {\n\t\t\t\t\twriteFrame(socket, { type: \"current\", ...(current === undefined ? {} : { credential: current }) });\n\t\t\t\t\tconst rawCommit = await reader.next();\n\t\t\t\t\tif (!isRequestObject(rawCommit)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Invalid modify response: expected an object.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst commit = rawCommit as CommitRequest;\n\t\t\t\t\tif (commit.action === \"abort\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(\n\t\t\t\t\t\t\t`credential-write ${providerId}`,\n\t\t\t\t\t\t\t\"Credential modification was aborted by the client.\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (commit.action === \"no_change\") return undefined;\n\t\t\t\t\tif (commit.action !== \"commit\") {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid modify response action.\");\n\t\t\t\t\t}\n\t\t\t\t\tif (!isCredential(commit.credential)) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, \"Invalid credential shape.\");\n\t\t\t\t\t}\n\t\t\t\t\tconst reference = findCredentialConfigReference(commit.credential);\n\t\t\t\t\tif (reference) {\n\t\t\t\t\t\tthrow new ChannelRefusal(`credential-write ${providerId}`, referenceRefusal(reference));\n\t\t\t\t\t}\n\t\t\t\t\tchanged = true;\n\t\t\t\t\treturn commit.credential;\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t\tif (changed) {\n\t\t\t\taudit(`credential-write ${providerId}`, \"Accepted: credential written through the sandbox channel.\");\n\t\t\t}\n\t\t\twriteFrame(socket, { ok: true, ...(result === undefined ? {} : { credential: result }) });\n\t\t};\n\n\t\tconst server = net.createServer((socket) => {\n\t\t\tif (sockets.size >= MAX_CONNECTIONS) {\n\t\t\t\trefuse(socket, \"credential-channel\", \"Credential channel connection limit reached.\");\n\t\t\t\tsocket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsockets.add(socket);\n\t\t\tconst controller = new AbortController();\n\t\t\tsocket.setTimeout(CONNECTION_IDLE_TIMEOUT_MS, () => {\n\t\t\t\taudit(\"credential-channel\", \"Refused: credential channel connection timed out.\");\n\t\t\t\tsocket.destroy(new Error(\"Credential channel connection timed out.\"));\n\t\t\t});\n\t\t\tsocket.on(\"close\", () => {\n\t\t\t\tsockets.delete(socket);\n\t\t\t\tcontroller.abort();\n\t\t\t});\n\t\t\tconst reader = new FrameReader(socket, \"Credential channel\", (detail) =>\n\t\t\t\taudit(\"credential-channel\", `Refused: ${detail}`),\n\t\t\t);\n\t\t\tconst operation = handleConnection(socket, reader, controller.signal)\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (error instanceof ChannelRefusal) {\n\t\t\t\t\t\trefuse(socket, error.command, error.message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trefuse(\n\t\t\t\t\t\tsocket,\n\t\t\t\t\t\t\"credential-channel\",\n\t\t\t\t\t\t\"The host credential operation failed.\",\n\t\t\t\t\t\t\"Host credential operation failed without exposing credential data.\",\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\toperations.delete(operation);\n\t\t\t\t\tsocket.end();\n\t\t\t\t});\n\t\t\toperations.add(operation);\n\t\t});\n\n\t\tconst cleanup = (): void => {\n\t\t\trmSync(options.socketPath, { force: true });\n\t\t\tif (options.cleanupDirectory) rmSync(options.cleanupDirectory, { force: true, recursive: true });\n\t\t};\n\t\tconst failStartup = (error: Error): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tserver.close();\n\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tserver.once(\"error\", failStartup);\n\t\tserver.listen(options.socketPath, () => {\n\t\t\ttry {\n\t\t\t\tchmodSync(options.socketPath, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tfailStartup(error instanceof Error ? error : new Error(String(error)));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tserver.off(\"error\", failStartup);\n\t\t\tresolve({\n\t\t\t\tclose: () => {\n\t\t\t\t\tif (closing) return closing;\n\t\t\t\t\tclosing = (async () => {\n\t\t\t\t\t\tfor (const socket of sockets) socket.destroy();\n\t\t\t\t\t\tawait new Promise<void>((resolveClose) => server.close(() => resolveClose()));\n\t\t\t\t\t\tawait Promise.allSettled([...operations]);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t})();\n\t\t\t\t\treturn closing;\n\t\t\t\t},\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type * as net from "node:net";
|
|
2
|
-
/**
|
|
3
|
-
* The framing both supervisor-owned RPC channels share.
|
|
4
|
-
*
|
|
5
|
-
* Extracted from `credential-proxy.ts` when the git credential channel needed the same
|
|
6
|
-
* reader. A second hand-written copy of a byte-bounded parser sitting on a socket that
|
|
7
|
-
* every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to
|
|
8
|
-
* prevent for tool contracts: a hardening applied to one copy and not the other is
|
|
9
|
-
* invisible until it matters. One implementation, named per channel only in its messages.
|
|
10
|
-
*/
|
|
11
|
-
export declare const MAX_FRAME_BYTES: number;
|
|
12
|
-
export declare function writeFrame(socket: net.Socket, response: object): void;
|
|
13
|
-
export declare function isRequestObject(value: unknown): value is Record<string, unknown>;
|
|
14
|
-
/**
|
|
15
|
-
* One request frame at a time, newline-delimited JSON, bounded in bytes.
|
|
16
|
-
*
|
|
17
|
-
* Pipelining is refused rather than queued: a channel that answers a second request before
|
|
18
|
-
* the first is served makes the audit tail ambiguous about which frame a refusal belonged
|
|
19
|
-
* to, and neither channel has a caller that needs it.
|
|
20
|
-
*/
|
|
21
|
-
export declare class FrameReader {
|
|
22
|
-
private readonly socket;
|
|
23
|
-
private readonly channelName;
|
|
24
|
-
private readonly onProtocolRefusal;
|
|
25
|
-
private buffer;
|
|
26
|
-
private readonly queued;
|
|
27
|
-
private readonly waiting;
|
|
28
|
-
private terminalError;
|
|
29
|
-
constructor(socket: net.Socket, channelName: string, onProtocolRefusal: (detail: string) => void);
|
|
30
|
-
next(): Promise<unknown>;
|
|
31
|
-
private push;
|
|
32
|
-
private rejectOversizedFrame;
|
|
33
|
-
private fail;
|
|
34
|
-
}
|
|
35
|
-
//# sourceMappingURL=framing.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"framing.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/framing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAErC;;;;;;;;GAQG;AAEH,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,wBAAgB,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAOrE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEhF;AAED;;;;;;GAMG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAC7D,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4E;IACpG,OAAO,CAAC,aAAa,CAAoB;IAEzC,YAAY,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EAO/F;IAED,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAKvB;IAED,OAAO,CAAC,IAAI;IAyCZ,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,IAAI;CAKZ","sourcesContent":["import type * as net from \"node:net\";\n\n/**\n * The framing both supervisor-owned RPC channels share.\n *\n * Extracted from `credential-proxy.ts` when the git credential channel needed the same\n * reader. A second hand-written copy of a byte-bounded parser sitting on a socket that\n * every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to\n * prevent for tool contracts: a hardening applied to one copy and not the other is\n * invisible until it matters. One implementation, named per channel only in its messages.\n */\n\nexport const MAX_FRAME_BYTES = 64 * 1024;\n\nexport function writeFrame(socket: net.Socket, response: object): void {\n\tif (socket.destroyed) return;\n\tconst frame = Buffer.from(`${JSON.stringify(response)}\\n`, \"utf8\");\n\tif (frame.length > MAX_FRAME_BYTES) {\n\t\tthrow new Error(\"Channel response exceeded its byte limit.\");\n\t}\n\tsocket.write(frame);\n}\n\nexport function isRequestObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * One request frame at a time, newline-delimited JSON, bounded in bytes.\n *\n * Pipelining is refused rather than queued: a channel that answers a second request before\n * the first is served makes the audit tail ambiguous about which frame a refusal belonged\n * to, and neither channel has a caller that needs it.\n */\nexport class FrameReader {\n\tprivate readonly socket: net.Socket;\n\tprivate readonly channelName: string;\n\tprivate readonly onProtocolRefusal: (detail: string) => void;\n\tprivate buffer = Buffer.alloc(0);\n\tprivate readonly queued: unknown[] = [];\n\tprivate readonly waiting: Array<{ resolve(value: unknown): void; reject(error: Error): void }> = [];\n\tprivate terminalError: Error | undefined;\n\n\tconstructor(socket: net.Socket, channelName: string, onProtocolRefusal: (detail: string) => void) {\n\t\tthis.socket = socket;\n\t\tthis.channelName = channelName;\n\t\tthis.onProtocolRefusal = onProtocolRefusal;\n\t\tsocket.on(\"data\", (chunk: Buffer) => this.push(chunk));\n\t\tsocket.on(\"close\", () => this.fail(new Error(`${channelName} client disconnected.`)));\n\t\tsocket.on(\"error\", (error) => this.fail(error));\n\t}\n\n\tnext(): Promise<unknown> {\n\t\tconst queued = this.queued.shift();\n\t\tif (queued !== undefined) return Promise.resolve(queued);\n\t\tif (this.terminalError) return Promise.reject(this.terminalError);\n\t\treturn new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));\n\t}\n\n\tprivate push(chunk: Buffer): void {\n\t\tif (this.terminalError) return;\n\t\tthis.buffer = Buffer.concat([this.buffer, chunk]);\n\t\twhile (true) {\n\t\t\tconst newline = this.buffer.indexOf(0x0a);\n\t\t\tif (newline < 0) {\n\t\t\t\tif (this.buffer.length > MAX_FRAME_BYTES) this.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (newline > MAX_FRAME_BYTES) {\n\t\t\t\tthis.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst frame = this.buffer.subarray(0, newline);\n\t\t\tthis.buffer = this.buffer.subarray(newline + 1);\n\t\t\tif (frame.length === 0) continue;\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(frame.toString(\"utf8\")) as unknown;\n\t\t\t} catch {\n\t\t\t\tthis.onProtocolRefusal(\"Invalid request frame: not JSON.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Invalid request frame: not JSON.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an invalid JSON frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (waiter) {\n\t\t\t\twaiter.resolve(value);\n\t\t\t} else if (this.queued.length === 0) {\n\t\t\t\tthis.queued.push(value);\n\t\t\t} else {\n\t\t\t\tthis.onProtocolRefusal(\"Unexpected pipelined request frame.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Unexpected pipelined request frame.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an unexpected pipelined frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate rejectOversizedFrame(): void {\n\t\tthis.onProtocolRefusal(\"Request frame exceeded the 64 KiB byte limit.\");\n\t\twriteFrame(this.socket, { ok: false, error: \"Request frame is too large.\" });\n\t\tthis.fail(new Error(`${this.channelName} frame exceeded its byte limit.`));\n\t\tthis.socket.end();\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.terminalError) return;\n\t\tthis.terminalError = error;\n\t\tfor (const waiter of this.waiting.splice(0)) waiter.reject(error);\n\t}\n}\n"]}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The framing both supervisor-owned RPC channels share.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from `credential-proxy.ts` when the git credential channel needed the same
|
|
5
|
-
* reader. A second hand-written copy of a byte-bounded parser sitting on a socket that
|
|
6
|
-
* every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to
|
|
7
|
-
* prevent for tool contracts: a hardening applied to one copy and not the other is
|
|
8
|
-
* invisible until it matters. One implementation, named per channel only in its messages.
|
|
9
|
-
*/
|
|
10
|
-
export const MAX_FRAME_BYTES = 64 * 1024;
|
|
11
|
-
export function writeFrame(socket, response) {
|
|
12
|
-
if (socket.destroyed)
|
|
13
|
-
return;
|
|
14
|
-
const frame = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
|
|
15
|
-
if (frame.length > MAX_FRAME_BYTES) {
|
|
16
|
-
throw new Error("Channel response exceeded its byte limit.");
|
|
17
|
-
}
|
|
18
|
-
socket.write(frame);
|
|
19
|
-
}
|
|
20
|
-
export function isRequestObject(value) {
|
|
21
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* One request frame at a time, newline-delimited JSON, bounded in bytes.
|
|
25
|
-
*
|
|
26
|
-
* Pipelining is refused rather than queued: a channel that answers a second request before
|
|
27
|
-
* the first is served makes the audit tail ambiguous about which frame a refusal belonged
|
|
28
|
-
* to, and neither channel has a caller that needs it.
|
|
29
|
-
*/
|
|
30
|
-
export class FrameReader {
|
|
31
|
-
socket;
|
|
32
|
-
channelName;
|
|
33
|
-
onProtocolRefusal;
|
|
34
|
-
buffer = Buffer.alloc(0);
|
|
35
|
-
queued = [];
|
|
36
|
-
waiting = [];
|
|
37
|
-
terminalError;
|
|
38
|
-
constructor(socket, channelName, onProtocolRefusal) {
|
|
39
|
-
this.socket = socket;
|
|
40
|
-
this.channelName = channelName;
|
|
41
|
-
this.onProtocolRefusal = onProtocolRefusal;
|
|
42
|
-
socket.on("data", (chunk) => this.push(chunk));
|
|
43
|
-
socket.on("close", () => this.fail(new Error(`${channelName} client disconnected.`)));
|
|
44
|
-
socket.on("error", (error) => this.fail(error));
|
|
45
|
-
}
|
|
46
|
-
next() {
|
|
47
|
-
const queued = this.queued.shift();
|
|
48
|
-
if (queued !== undefined)
|
|
49
|
-
return Promise.resolve(queued);
|
|
50
|
-
if (this.terminalError)
|
|
51
|
-
return Promise.reject(this.terminalError);
|
|
52
|
-
return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
53
|
-
}
|
|
54
|
-
push(chunk) {
|
|
55
|
-
if (this.terminalError)
|
|
56
|
-
return;
|
|
57
|
-
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
58
|
-
while (true) {
|
|
59
|
-
const newline = this.buffer.indexOf(0x0a);
|
|
60
|
-
if (newline < 0) {
|
|
61
|
-
if (this.buffer.length > MAX_FRAME_BYTES)
|
|
62
|
-
this.rejectOversizedFrame();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (newline > MAX_FRAME_BYTES) {
|
|
66
|
-
this.rejectOversizedFrame();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const frame = this.buffer.subarray(0, newline);
|
|
70
|
-
this.buffer = this.buffer.subarray(newline + 1);
|
|
71
|
-
if (frame.length === 0)
|
|
72
|
-
continue;
|
|
73
|
-
let value;
|
|
74
|
-
try {
|
|
75
|
-
value = JSON.parse(frame.toString("utf8"));
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
this.onProtocolRefusal("Invalid request frame: not JSON.");
|
|
79
|
-
writeFrame(this.socket, { ok: false, error: "Invalid request frame: not JSON." });
|
|
80
|
-
this.fail(new Error(`${this.channelName} received an invalid JSON frame.`));
|
|
81
|
-
this.socket.end();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const waiter = this.waiting.shift();
|
|
85
|
-
if (waiter) {
|
|
86
|
-
waiter.resolve(value);
|
|
87
|
-
}
|
|
88
|
-
else if (this.queued.length === 0) {
|
|
89
|
-
this.queued.push(value);
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
this.onProtocolRefusal("Unexpected pipelined request frame.");
|
|
93
|
-
writeFrame(this.socket, { ok: false, error: "Unexpected pipelined request frame." });
|
|
94
|
-
this.fail(new Error(`${this.channelName} received an unexpected pipelined frame.`));
|
|
95
|
-
this.socket.end();
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
rejectOversizedFrame() {
|
|
101
|
-
this.onProtocolRefusal("Request frame exceeded the 64 KiB byte limit.");
|
|
102
|
-
writeFrame(this.socket, { ok: false, error: "Request frame is too large." });
|
|
103
|
-
this.fail(new Error(`${this.channelName} frame exceeded its byte limit.`));
|
|
104
|
-
this.socket.end();
|
|
105
|
-
}
|
|
106
|
-
fail(error) {
|
|
107
|
-
if (this.terminalError)
|
|
108
|
-
return;
|
|
109
|
-
this.terminalError = error;
|
|
110
|
-
for (const waiter of this.waiting.splice(0))
|
|
111
|
-
waiter.reject(error);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
//# sourceMappingURL=framing.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"framing.js","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/framing.ts"],"names":[],"mappings":"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","sourcesContent":["import type * as net from \"node:net\";\n\n/**\n * The framing both supervisor-owned RPC channels share.\n *\n * Extracted from `credential-proxy.ts` when the git credential channel needed the same\n * reader. A second hand-written copy of a byte-bounded parser sitting on a socket that\n * every process inside the sandbox can reach is exactly the divergence ADR 0010 exists to\n * prevent for tool contracts: a hardening applied to one copy and not the other is\n * invisible until it matters. One implementation, named per channel only in its messages.\n */\n\nexport const MAX_FRAME_BYTES = 64 * 1024;\n\nexport function writeFrame(socket: net.Socket, response: object): void {\n\tif (socket.destroyed) return;\n\tconst frame = Buffer.from(`${JSON.stringify(response)}\\n`, \"utf8\");\n\tif (frame.length > MAX_FRAME_BYTES) {\n\t\tthrow new Error(\"Channel response exceeded its byte limit.\");\n\t}\n\tsocket.write(frame);\n}\n\nexport function isRequestObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * One request frame at a time, newline-delimited JSON, bounded in bytes.\n *\n * Pipelining is refused rather than queued: a channel that answers a second request before\n * the first is served makes the audit tail ambiguous about which frame a refusal belonged\n * to, and neither channel has a caller that needs it.\n */\nexport class FrameReader {\n\tprivate readonly socket: net.Socket;\n\tprivate readonly channelName: string;\n\tprivate readonly onProtocolRefusal: (detail: string) => void;\n\tprivate buffer = Buffer.alloc(0);\n\tprivate readonly queued: unknown[] = [];\n\tprivate readonly waiting: Array<{ resolve(value: unknown): void; reject(error: Error): void }> = [];\n\tprivate terminalError: Error | undefined;\n\n\tconstructor(socket: net.Socket, channelName: string, onProtocolRefusal: (detail: string) => void) {\n\t\tthis.socket = socket;\n\t\tthis.channelName = channelName;\n\t\tthis.onProtocolRefusal = onProtocolRefusal;\n\t\tsocket.on(\"data\", (chunk: Buffer) => this.push(chunk));\n\t\tsocket.on(\"close\", () => this.fail(new Error(`${channelName} client disconnected.`)));\n\t\tsocket.on(\"error\", (error) => this.fail(error));\n\t}\n\n\tnext(): Promise<unknown> {\n\t\tconst queued = this.queued.shift();\n\t\tif (queued !== undefined) return Promise.resolve(queued);\n\t\tif (this.terminalError) return Promise.reject(this.terminalError);\n\t\treturn new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));\n\t}\n\n\tprivate push(chunk: Buffer): void {\n\t\tif (this.terminalError) return;\n\t\tthis.buffer = Buffer.concat([this.buffer, chunk]);\n\t\twhile (true) {\n\t\t\tconst newline = this.buffer.indexOf(0x0a);\n\t\t\tif (newline < 0) {\n\t\t\t\tif (this.buffer.length > MAX_FRAME_BYTES) this.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (newline > MAX_FRAME_BYTES) {\n\t\t\t\tthis.rejectOversizedFrame();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst frame = this.buffer.subarray(0, newline);\n\t\t\tthis.buffer = this.buffer.subarray(newline + 1);\n\t\t\tif (frame.length === 0) continue;\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(frame.toString(\"utf8\")) as unknown;\n\t\t\t} catch {\n\t\t\t\tthis.onProtocolRefusal(\"Invalid request frame: not JSON.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Invalid request frame: not JSON.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an invalid JSON frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (waiter) {\n\t\t\t\twaiter.resolve(value);\n\t\t\t} else if (this.queued.length === 0) {\n\t\t\t\tthis.queued.push(value);\n\t\t\t} else {\n\t\t\t\tthis.onProtocolRefusal(\"Unexpected pipelined request frame.\");\n\t\t\t\twriteFrame(this.socket, { ok: false, error: \"Unexpected pipelined request frame.\" });\n\t\t\t\tthis.fail(new Error(`${this.channelName} received an unexpected pipelined frame.`));\n\t\t\t\tthis.socket.end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate rejectOversizedFrame(): void {\n\t\tthis.onProtocolRefusal(\"Request frame exceeded the 64 KiB byte limit.\");\n\t\twriteFrame(this.socket, { ok: false, error: \"Request frame is too large.\" });\n\t\tthis.fail(new Error(`${this.channelName} frame exceeded its byte limit.`));\n\t\tthis.socket.end();\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.terminalError) return;\n\t\tthis.terminalError = error;\n\t\tfor (const waiter of this.waiting.splice(0)) waiter.reject(error);\n\t}\n}\n"]}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { type GitCredential, type GitCredentialRequest } from "./git-credential-proxy.ts";
|
|
2
|
-
/** Env var naming the child-side socket the helper talks to. */
|
|
3
|
-
export declare const GIT_CREDENTIAL_SOCKET_VARIABLE = "APEX_GIT_CREDENTIAL_PATH";
|
|
4
|
-
export interface GitCredentialChannelPaths {
|
|
5
|
-
readonly hostSocketDirectory: string;
|
|
6
|
-
readonly hostSocketPath: string;
|
|
7
|
-
readonly childSocketPath: string;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Where the git credential socket lives on each side, mirroring the credential channel's
|
|
11
|
-
* own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace
|
|
12
|
-
* on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux
|
|
13
|
-
* because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.
|
|
14
|
-
*/
|
|
15
|
-
export declare function resolveGitCredentialChannelPaths(): GitCredentialChannelPaths;
|
|
16
|
-
/**
|
|
17
|
-
* Where the helper lives, derived rather than passed.
|
|
18
|
-
*
|
|
19
|
-
* The supervisor writes `credential.helper` into the projected git config before the
|
|
20
|
-
* platform backend exists, and the backend writes the file itself. Deriving both from the
|
|
21
|
-
* state directory is what keeps the config from naming a path nothing ever created.
|
|
22
|
-
*/
|
|
23
|
-
export declare function gitCredentialHelperPath(stateDirectory: string): string;
|
|
24
|
-
/** The `credential.helper` value naming that helper, run under the supervisor's own node. */
|
|
25
|
-
export declare function gitCredentialHelperCommand(stateDirectory: string, nodePath?: string): string;
|
|
26
|
-
/** Write the helper into a directory the child can read, returning its path. */
|
|
27
|
-
export declare function writeGitCredentialHelper(directory: string): string;
|
|
28
|
-
/**
|
|
29
|
-
* Resolve a credential on the host, through the host's own git configuration.
|
|
30
|
-
*
|
|
31
|
-
* `git credential fill` rather than reading any particular store, because the answer may
|
|
32
|
-
* come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git
|
|
33
|
-
* knows which of those this host is configured to ask. Reimplementing that resolution
|
|
34
|
-
* would be a second, quietly diverging copy of it.
|
|
35
|
-
*
|
|
36
|
-
* Two things about *where* it runs are part of the boundary rather than housekeeping. It
|
|
37
|
-
* runs in a private empty directory, never in whatever directory the supervisor happened to
|
|
38
|
-
* be started from, because a repository's own `credential.helper` is a command git would
|
|
39
|
-
* otherwise execute on the host. And it runs without the four variables that would point it
|
|
40
|
-
* back at such a repository. The request is re-parsed here too: this function serializes
|
|
41
|
-
* into git's line protocol, so it refuses anything that could rewrite the identity it was
|
|
42
|
-
* asked for, whether or not the proxy already checked.
|
|
43
|
-
*/
|
|
44
|
-
export declare function fillHostGitCredential(request: GitCredentialRequest, options?: {
|
|
45
|
-
environment?: NodeJS.ProcessEnv;
|
|
46
|
-
}): Promise<GitCredential | undefined>;
|
|
47
|
-
//# sourceMappingURL=git-credential-helper.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"git-credential-helper.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/rpc/git-credential-helper.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAA6B,MAAM,2BAA2B,CAAC;AAErH,gEAAgE;AAChE,eAAO,MAAM,8BAA8B,6BAA6B,CAAC;AAwEzE,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,IAAI,yBAAyB,CAW5E;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,6FAA6F;AAC7F,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,SAAmB,GAAG,MAAM,CAEtG;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIlE;AAqDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,oBAAoB,EAC7B,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAE,GAC3C,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CA+CpC","sourcesContent":["import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { supervisorTempDirectory } from \"../supervisor-temp.ts\";\nimport { type GitCredential, type GitCredentialRequest, parseGitCredentialRequest } from \"./git-credential-proxy.ts\";\n\n/** Env var naming the child-side socket the helper talks to. */\nexport const GIT_CREDENTIAL_SOCKET_VARIABLE = \"APEX_GIT_CREDENTIAL_PATH\";\n\n/**\n * The credential helper git runs inside the sandbox.\n *\n * `.cjs` for the same reason the network relay is: a plain `.js` under a workspace whose\n * package.json declares `\"type\": \"module\"` is parsed as ESM and dies on `require`.\n *\n * It answers `get` and nothing else. `store` and `erase` exit silently and successfully,\n * which is what tells git the credential was handled and stops it falling through to\n * another helper. Writing is deliberately not served: ADR 0015 keeps credential mutation\n * an explicit host operation, and a channel that let the child rewrite the host's store\n * would be a way out of the boundary rather than a way to work inside it.\n *\n * Any failure exits 0 having printed nothing, which git reads as \"this helper has no\n * credential\" and handles normally. Exiting non-zero would surface as a git error that\n * looks like a repository problem rather than a refused release.\n */\nconst HELPER_SOURCE = `\nconst net = require(\"node:net\");\n\nconst operation = process.argv[2];\nif (operation !== \"get\") process.exit(0);\n\nlet input = \"\";\nprocess.stdin.setEncoding(\"utf8\");\nprocess.stdin.on(\"data\", (chunk) => {\n\tinput += chunk;\n});\nprocess.stdin.on(\"end\", () => {\n\tconst request = {};\n\tfor (const line of input.split(\"\\\\n\")) {\n\t\tconst separator = line.indexOf(\"=\");\n\t\tif (separator > 0) request[line.slice(0, separator)] = line.slice(separator + 1).trim();\n\t}\n\tconst socketPath = process.env.${GIT_CREDENTIAL_SOCKET_VARIABLE};\n\tif (!socketPath || !request.host) process.exit(0);\n\n\tconst socket = net.connect(socketPath);\n\tlet buffer = \"\";\n\tconst giveUp = () => {\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t};\n\tsocket.on(\"connect\", () => {\n\t\tsocket.write(JSON.stringify({ op: \"get\", protocol: request.protocol || \"https\", host: request.host }) + \"\\\\n\");\n\t});\n\tsocket.on(\"data\", (chunk) => {\n\t\tbuffer += chunk.toString();\n\t\tconst newline = buffer.indexOf(\"\\\\n\");\n\t\tif (newline < 0) return;\n\t\tlet response;\n\t\ttry {\n\t\t\tresponse = JSON.parse(buffer.slice(0, newline));\n\t\t} catch {\n\t\t\tgiveUp();\n\t\t\treturn;\n\t\t}\n\t\tif (response && response.ok) {\n\t\t\tprocess.stdout.write(\"username=\" + response.username + \"\\\\n\");\n\t\t\tprocess.stdout.write(\"password=\" + response.password + \"\\\\n\");\n\t\t}\n\t\tsocket.destroy();\n\t\tprocess.exit(0);\n\t});\n\tsocket.on(\"error\", giveUp);\n});\n`.trim();\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface GitCredentialChannelPaths {\n\treadonly hostSocketDirectory: string;\n\treadonly hostSocketPath: string;\n\treadonly childSocketPath: string;\n}\n\n/**\n * Where the git credential socket lives on each side, mirroring the credential channel's\n * own resolution. TMPDIR is deliberately not honoured: it can point inside the workspace\n * on macOS, which the sandbox may write. The child-side path sits under `/home` on Linux\n * because the `--tmpfs /home` is the only writable mount when bwrap creates the mountpoint.\n */\nexport function resolveGitCredentialChannelPaths(): GitCredentialChannelPaths {\n\tconst hostSocketDirectory = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-${process.pid}-`), {\n\t\tencoding: \"utf8\",\n\t});\n\tchmodSync(hostSocketDirectory, 0o700);\n\tconst hostSocketPath = join(hostSocketDirectory, \"channel.sock\");\n\tif (Buffer.byteLength(hostSocketPath) + 1 > SUN_PATH_LIMIT) {\n\t\tthrow new Error(\"Git credential channel path exceeds the Unix socket path limit.\");\n\t}\n\tconst childSocketPath = process.platform === \"linux\" ? \"/home/apex-git-credential-channel.sock\" : hostSocketPath;\n\treturn { hostSocketDirectory, hostSocketPath, childSocketPath };\n}\n\n/**\n * Where the helper lives, derived rather than passed.\n *\n * The supervisor writes `credential.helper` into the projected git config before the\n * platform backend exists, and the backend writes the file itself. Deriving both from the\n * state directory is what keeps the config from naming a path nothing ever created.\n */\nexport function gitCredentialHelperPath(stateDirectory: string): string {\n\treturn join(stateDirectory, \"git-credential-apex.cjs\");\n}\n\n/** The `credential.helper` value naming that helper, run under the supervisor's own node. */\nexport function gitCredentialHelperCommand(stateDirectory: string, nodePath = process.execPath): string {\n\treturn `!\"${nodePath}\" \"${gitCredentialHelperPath(stateDirectory)}\"`;\n}\n\n/** Write the helper into a directory the child can read, returning its path. */\nexport function writeGitCredentialHelper(directory: string): string {\n\tconst path = gitCredentialHelperPath(directory);\n\twriteFileSync(path, HELPER_SOURCE);\n\treturn path;\n}\n\n/**\n * The environment variables that decide which repository git is standing in, and which\n * config it reads.\n *\n * `credential.helper` is a shell command git runs on whichever machine resolves the\n * credential -- the host, here. Any of these four lets a repository the agent cloned choose\n * that command, so a credential fill that inherits them is a host execution primitive with\n * a socket in front of it.\n *\n * `HOME` and the global and system config are deliberately kept. They are how `gh`,\n * libsecret, the macOS keychain and a plain `credential.helper` in `--global` are found,\n * and they belong to the human running the supervisor rather than to any repository.\n */\nconst REPOSITORY_DISCOVERY_VARIABLES = [\"GIT_DIR\", \"GIT_WORK_TREE\", \"GIT_INDEX_FILE\", \"GIT_CONFIG\"] as const;\n\ninterface PrivateFillDirectory {\n\t/** Where git runs: empty, 0700, and inside no repository. */\n\treadonly cwd: string;\n\t/** Its parent, so git's upward search for a repository stops before leaving it. */\n\treadonly ceiling: string;\n\treadonly dispose: () => void;\n}\n\nfunction createPrivateFillDirectory(): PrivateFillDirectory {\n\tconst base = mkdtempSync(join(supervisorTempDirectory(), `apex-gitcred-fill-${process.pid}-`), { encoding: \"utf8\" });\n\tchmodSync(base, 0o700);\n\tconst cwd = join(base, \"empty\");\n\tmkdirSync(cwd, { mode: 0o700 });\n\t// Resolved, because git ignores a ceiling entry that would need symlink resolution and\n\t// `/tmp` is a symlink on macOS.\n\tlet ceiling = base;\n\ttry {\n\t\tceiling = realpathSync(base);\n\t} catch {\n\t\t// A ceiling that does not resolve is a weaker second guard, not a broken fill; the\n\t\t// empty cwd is what does the work.\n\t}\n\treturn {\n\t\tcwd,\n\t\tceiling,\n\t\tdispose: () => {\n\t\t\ttry {\n\t\t\t\trmSync(base, { force: true, recursive: true });\n\t\t\t} catch {\n\t\t\t\t// A leftover empty directory under the supervisor's own temp root is not worth\n\t\t\t\t// failing a credential fill over.\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Resolve a credential on the host, through the host's own git configuration.\n *\n * `git credential fill` rather than reading any particular store, because the answer may\n * come from `gh`'s helper, libsecret, the macOS keychain, or a plain file, and only git\n * knows which of those this host is configured to ask. Reimplementing that resolution\n * would be a second, quietly diverging copy of it.\n *\n * Two things about *where* it runs are part of the boundary rather than housekeeping. It\n * runs in a private empty directory, never in whatever directory the supervisor happened to\n * be started from, because a repository's own `credential.helper` is a command git would\n * otherwise execute on the host. And it runs without the four variables that would point it\n * back at such a repository. The request is re-parsed here too: this function serializes\n * into git's line protocol, so it refuses anything that could rewrite the identity it was\n * asked for, whether or not the proxy already checked.\n */\nexport async function fillHostGitCredential(\n\trequest: GitCredentialRequest,\n\toptions?: { environment?: NodeJS.ProcessEnv },\n): Promise<GitCredential | undefined> {\n\tconst parsed = parseGitCredentialRequest({ protocol: request.protocol, host: request.host });\n\tif (!parsed.ok) return undefined;\n\tconst identity = parsed.request;\n\n\tconst { spawn } = await import(\"node:child_process\");\n\tconst directory = createPrivateFillDirectory();\n\tconst environment: NodeJS.ProcessEnv = { ...(options?.environment ?? process.env) };\n\tfor (const variable of REPOSITORY_DISCOVERY_VARIABLES) delete environment[variable];\n\tenvironment.GIT_CEILING_DIRECTORIES = directory.ceiling;\n\n\treturn new Promise((resolve) => {\n\t\tlet settled = false;\n\t\tconst finish = (credential: GitCredential | undefined): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tdirectory.dispose();\n\t\t\tresolve(credential);\n\t\t};\n\t\tconst child = spawn(\"git\", [\"credential\", \"fill\"], {\n\t\t\tcwd: directory.cwd,\n\t\t\tenv: environment,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"ignore\"],\n\t\t});\n\t\tlet stdout = \"\";\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.on(\"error\", () => finish(undefined));\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code !== 0) {\n\t\t\t\tfinish(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst fields = new Map<string, string>();\n\t\t\tfor (const line of stdout.split(\"\\n\")) {\n\t\t\t\tconst separator = line.indexOf(\"=\");\n\t\t\t\tif (separator > 0) fields.set(line.slice(0, separator), line.slice(separator + 1).trim());\n\t\t\t}\n\t\t\tconst username = fields.get(\"username\");\n\t\t\tconst password = fields.get(\"password\");\n\t\t\tfinish(username && password ? { username, password } : undefined);\n\t\t});\n\t\tchild.stdin.on(\"error\", () => finish(undefined));\n\t\tchild.stdin.end(`protocol=${identity.protocol}\\nhost=${identity.host}\\n\\n`);\n\t});\n}\n"]}
|