@openlatch/client 0.5.4 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/lib/postinstall.js +4 -0
- package/lib/resolve-binary.js +173 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -149,6 +149,12 @@ Pick a [good first issue](https://github.com/OpenLatch/openlatch-client/labels/g
|
|
|
149
149
|
|
|
150
150
|
- Report vulnerabilities to **security@openlatch.ai**; private vulnerability reporting is enabled on this repository
|
|
151
151
|
- Every release binary carries a minisign signature, which the client verifies before it updates itself; macOS and Linux binaries also carry Sigstore signatures. What is and is not signed: [SECURITY.md](SECURITY.md)
|
|
152
|
+
- Binary replacement is also policy-cache safe. Self-update and the npm/npx
|
|
153
|
+
launcher ask the candidate executable to load the exact resident bundle
|
|
154
|
+
before activation and retain a previous evaluator/binary pair if it cannot.
|
|
155
|
+
This proof is local and works offline; an update never fetches on the hook
|
|
156
|
+
verdict path. npm launcher state lives under `OPENLATCH_DIR` as
|
|
157
|
+
`npm-launcher.json` plus versioned `npm-bin/` pairs.
|
|
152
158
|
- Full policy: [SECURITY.md](SECURITY.md)
|
|
153
159
|
|
|
154
160
|
## License
|
package/lib/postinstall.js
CHANGED
|
@@ -98,6 +98,10 @@ function downloadBinaries() {
|
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
100
|
downloadBinaries();
|
|
101
|
+
// Package-manager installs use the same cache-reader proof and durable
|
|
102
|
+
// current/previous pair as the runtime launcher. Failure stays non-fatal to
|
|
103
|
+
// npm itself, but an incompatible candidate is not activated.
|
|
104
|
+
require("./resolve-binary").activateCandidate();
|
|
101
105
|
} catch (err) {
|
|
102
106
|
// Never fail the npm install — fail-open principle
|
|
103
107
|
console.warn(`openlatch: postinstall warning — ${err.message}`);
|
package/lib/resolve-binary.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const os = require("os");
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const crypto = require("crypto");
|
|
13
|
+
const { spawnSync } = require("child_process");
|
|
11
14
|
|
|
12
15
|
const PLATFORMS = {
|
|
13
16
|
"darwin-arm64": "@openlatch/client-darwin-arm64",
|
|
@@ -17,7 +20,7 @@ const PLATFORMS = {
|
|
|
17
20
|
"win32-x64": "@openlatch/client-win32-x64",
|
|
18
21
|
};
|
|
19
22
|
|
|
20
|
-
function
|
|
23
|
+
function resolveCandidate(baseName) {
|
|
21
24
|
const platformKey = `${os.platform()}-${os.arch()}`;
|
|
22
25
|
const pkg = PLATFORMS[platformKey];
|
|
23
26
|
|
|
@@ -42,4 +45,172 @@ function resolveBinary(baseName) {
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
function openlatchDir() {
|
|
49
|
+
return process.env.OPENLATCH_DIR || path.join(os.homedir(), ".openlatch");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function launcherStatePath() {
|
|
53
|
+
return path.join(openlatchDir(), "npm-launcher.json");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readState() {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(fs.readFileSync(launcherStatePath(), "utf8"));
|
|
59
|
+
} catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeState(state) {
|
|
65
|
+
const file = launcherStatePath();
|
|
66
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
67
|
+
const temporary = `${file}.tmp`;
|
|
68
|
+
fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
69
|
+
fs.renameSync(temporary, file);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function preservePreGuardEvaluator(state) {
|
|
73
|
+
if (state.current) return state;
|
|
74
|
+
try {
|
|
75
|
+
const pid = fs.readFileSync(path.join(openlatchDir(), "daemon.pid"), "utf8").trim();
|
|
76
|
+
if (!/^\d+$/.test(pid)) return state;
|
|
77
|
+
const ext = os.platform() === "win32" ? ".exe" : "";
|
|
78
|
+
let runningExe;
|
|
79
|
+
let runningDir;
|
|
80
|
+
if (os.platform() === "linux") {
|
|
81
|
+
runningExe = `/proc/${pid}/exe`;
|
|
82
|
+
runningDir = path.dirname(fs.readlinkSync(runningExe).replace(/ \(deleted\)$/, ""));
|
|
83
|
+
} else if (os.platform() === "darwin") {
|
|
84
|
+
const query = spawnSync("ps", ["-p", pid, "-o", "comm="], { encoding: "utf8" });
|
|
85
|
+
runningExe = (query.stdout || "").trim();
|
|
86
|
+
runningDir = path.dirname(runningExe);
|
|
87
|
+
} else if (os.platform() === "win32") {
|
|
88
|
+
const query = spawnSync(
|
|
89
|
+
"powershell.exe",
|
|
90
|
+
["-NoProfile", "-Command", `(Get-Process -Id ${pid}).Path`],
|
|
91
|
+
{ encoding: "utf8" }
|
|
92
|
+
);
|
|
93
|
+
runningExe = (query.stdout || "").trim();
|
|
94
|
+
runningDir = path.dirname(runningExe);
|
|
95
|
+
} else {
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
if (!runningExe) return state;
|
|
99
|
+
const runningHook = process.env.OPENLATCH_HOOK_BIN || path.join(runningDir, `openlatch-hook${ext}`);
|
|
100
|
+
if (!fs.existsSync(runningHook)) return state;
|
|
101
|
+
const root = path.join(openlatchDir(), "npm-bin", `previous-${Date.now()}`);
|
|
102
|
+
fs.mkdirSync(root, { recursive: true });
|
|
103
|
+
fs.copyFileSync(runningExe, path.join(root, `openlatch${ext}`));
|
|
104
|
+
fs.copyFileSync(runningHook, path.join(root, `openlatch-hook${ext}`));
|
|
105
|
+
fs.chmodSync(path.join(root, `openlatch${ext}`), 0o755);
|
|
106
|
+
fs.chmodSync(path.join(root, `openlatch-hook${ext}`), 0o755);
|
|
107
|
+
const preserved = { ...state, current: root, previous: null };
|
|
108
|
+
writeState(preserved);
|
|
109
|
+
return preserved;
|
|
110
|
+
} catch {
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function replacePairAtRecordedPaths(executable, hook, state) {
|
|
116
|
+
const ext = os.platform() === "win32" ? ".exe" : "";
|
|
117
|
+
if (!state.current) {
|
|
118
|
+
// With no prior pair to restore, make direct supervision/hook paths fail
|
|
119
|
+
// closed instead of letting an unreadable evaluator start fail-open.
|
|
120
|
+
for (const candidate of [executable, hook]) {
|
|
121
|
+
if (fs.existsSync(candidate)) fs.renameSync(candidate, `${candidate}.reader-blocked`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const [sourceName, destination] of [
|
|
126
|
+
[`openlatch${ext}`, executable],
|
|
127
|
+
[`openlatch-hook${ext}`, hook],
|
|
128
|
+
]) {
|
|
129
|
+
const source = path.join(state.current, sourceName);
|
|
130
|
+
const temporary = `${destination}.reader-restore`;
|
|
131
|
+
fs.copyFileSync(source, temporary);
|
|
132
|
+
if (os.platform() !== "win32") fs.chmodSync(temporary, 0o755);
|
|
133
|
+
// Windows rename does not replace an existing destination. The candidate
|
|
134
|
+
// is not running yet (this is postinstall), so remove it before installing
|
|
135
|
+
// the preserved bytes; POSIX follows the same explicit path.
|
|
136
|
+
fs.rmSync(destination, { force: true });
|
|
137
|
+
fs.renameSync(temporary, destination);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Copy a candidate pair into a durable current/previous launcher directory,
|
|
143
|
+
* then activate it only after the candidate itself proves resident-cache
|
|
144
|
+
* readability. npm and npx therefore share the native updater's guard instead
|
|
145
|
+
* of replacing the evaluator merely because package resolution changed.
|
|
146
|
+
*/
|
|
147
|
+
function activateCandidate() {
|
|
148
|
+
const executable = resolveCandidate("openlatch");
|
|
149
|
+
const hook = resolveCandidate("openlatch-hook");
|
|
150
|
+
if (!fs.existsSync(executable) || !fs.existsSync(hook)) return readState();
|
|
151
|
+
|
|
152
|
+
const stat = fs.statSync(executable);
|
|
153
|
+
const identity = crypto
|
|
154
|
+
.createHash("sha256")
|
|
155
|
+
.update(`${executable}:${stat.size}:${stat.mtimeMs}`)
|
|
156
|
+
.digest("hex")
|
|
157
|
+
.slice(0, 20);
|
|
158
|
+
const root = path.join(openlatchDir(), "npm-bin", identity);
|
|
159
|
+
const ext = os.platform() === "win32" ? ".exe" : "";
|
|
160
|
+
const stagedExe = path.join(root, `openlatch${ext}`);
|
|
161
|
+
const stagedHook = path.join(root, `openlatch-hook${ext}`);
|
|
162
|
+
const existing = readState();
|
|
163
|
+
if (existing.current === root && fs.existsSync(stagedExe) && fs.existsSync(stagedHook)) {
|
|
164
|
+
return existing;
|
|
165
|
+
}
|
|
166
|
+
fs.mkdirSync(root, { recursive: true });
|
|
167
|
+
fs.copyFileSync(executable, stagedExe);
|
|
168
|
+
fs.copyFileSync(hook, stagedHook);
|
|
169
|
+
if (os.platform() !== "win32") {
|
|
170
|
+
fs.chmodSync(stagedExe, 0o755);
|
|
171
|
+
fs.chmodSync(stagedHook, 0o755);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const proof = spawnSync(stagedExe, ["__reader-check", "--cache", openlatchDir()], {
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
timeout: 10000,
|
|
177
|
+
});
|
|
178
|
+
const state = preservePreGuardEvaluator(readState());
|
|
179
|
+
if (proof.status !== 0) {
|
|
180
|
+
if (state.current) {
|
|
181
|
+
replacePairAtRecordedPaths(executable, hook, state);
|
|
182
|
+
console.warn(
|
|
183
|
+
`openlatch: candidate reader check failed; preserving previous evaluator (${(proof.stderr || "").trim()})`
|
|
184
|
+
);
|
|
185
|
+
return state;
|
|
186
|
+
}
|
|
187
|
+
replacePairAtRecordedPaths(executable, hook, state);
|
|
188
|
+
throw new Error(
|
|
189
|
+
`candidate cannot read the resident policy cache and no previous evaluator is available: ${(proof.stderr || "reader check failed").trim()}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (state.current !== root) {
|
|
193
|
+
writeState({ current: root, previous: state.current || null, activated_at: new Date().toISOString() });
|
|
194
|
+
}
|
|
195
|
+
return readState();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resolveBinary(baseName) {
|
|
199
|
+
const ext = os.platform() === "win32" ? ".exe" : "";
|
|
200
|
+
// The main command is the activation boundary. The hook is latency-critical
|
|
201
|
+
// and only follows the already-activated pair.
|
|
202
|
+
const state = baseName === "openlatch" ? activateCandidate() : readState();
|
|
203
|
+
if (state.current) {
|
|
204
|
+
const selected = path.join(state.current, `${baseName}${ext}`);
|
|
205
|
+
if (fs.existsSync(selected)) return selected;
|
|
206
|
+
}
|
|
207
|
+
return resolveCandidate(baseName);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
resolveBinary,
|
|
212
|
+
resolveCandidate,
|
|
213
|
+
activateCandidate,
|
|
214
|
+
replacePairAtRecordedPaths,
|
|
215
|
+
PLATFORMS,
|
|
216
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlatch/client",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"postinstall": "node lib/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@openlatch/client-darwin-arm64": "0.5.
|
|
24
|
-
"@openlatch/client-darwin-x64": "0.5.
|
|
25
|
-
"@openlatch/client-linux-x64": "0.5.
|
|
26
|
-
"@openlatch/client-linux-arm64": "0.5.
|
|
27
|
-
"@openlatch/client-win32-x64": "0.5.
|
|
23
|
+
"@openlatch/client-darwin-arm64": "0.5.7",
|
|
24
|
+
"@openlatch/client-darwin-x64": "0.5.7",
|
|
25
|
+
"@openlatch/client-linux-x64": "0.5.7",
|
|
26
|
+
"@openlatch/client-linux-arm64": "0.5.7",
|
|
27
|
+
"@openlatch/client-win32-x64": "0.5.7"
|
|
28
28
|
}
|
|
29
29
|
}
|