@codyswann/lisa 4.72.6 → 4.73.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/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +19 -0
- package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +4 -2
- package/all/copy-overwrite/scripts/lib/history-secret-evidence-shape.mjs +398 -6
- package/all/copy-overwrite/scripts/lib/history-secret-evidence.mjs +175 -7
- package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +7 -1
- package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +11 -66
- package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +3 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +10 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +162 -121
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +8 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-scope.mjs +177 -0
- package/all/copy-overwrite/scripts/lib/npm-update-policy.mjs +73 -0
- package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +1 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-daemon.mjs +227 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-resource.mjs +176 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-storage.mjs +201 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql.mjs +220 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-runtime-contract.mjs +75 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-tool-downloads.mjs +187 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-tool-identity.mjs +288 -0
- package/all/copy-overwrite/scripts/lib/npm-update-rails-tools.mjs +292 -0
- package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +3 -0
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +1 -1
- package/all/copy-overwrite/scripts/lisa-scratch-run.sh +7 -4
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +148 -20
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +126 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +2 -1
- package/dist/core/lisa.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +79 -17
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +1 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +1 -1
- package/scripts/two-channel-couplings.json +1 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Selected native daemon observations never expose container environment or credentials. */
|
|
5
|
+
import { required } from "./npm-update-contract.mjs";
|
|
6
|
+
import { runProcess } from "./npm-update-process-core.mjs";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { publicFailure } from "./npm-update-invariants.mjs";
|
|
9
|
+
import {
|
|
10
|
+
assertDockerQualified,
|
|
11
|
+
runtimeTime,
|
|
12
|
+
} from "./npm-update-rails-tool-identity.mjs";
|
|
13
|
+
|
|
14
|
+
const FIELDS =
|
|
15
|
+
"{{json .Id}}\t{{json .Name}}\t{{json .Config.Labels}}\t{{json .Image}}\t{{json .State.Running}}\t{{json .Mounts}}\t{{json .NetworkSettings.Ports}}";
|
|
16
|
+
const fields = bytes =>
|
|
17
|
+
bytes
|
|
18
|
+
.toString()
|
|
19
|
+
.trim()
|
|
20
|
+
.split("\t")
|
|
21
|
+
.map(value => JSON.parse(value));
|
|
22
|
+
|
|
23
|
+
const COMMAND_PHASES = Object.freeze({
|
|
24
|
+
pull: "Docker pull",
|
|
25
|
+
image: "Docker image",
|
|
26
|
+
ps: "Docker census",
|
|
27
|
+
create: "Docker create",
|
|
28
|
+
start: "Docker start",
|
|
29
|
+
stop: "Docker stop",
|
|
30
|
+
container: "Docker container",
|
|
31
|
+
exec: "MySQL query",
|
|
32
|
+
});
|
|
33
|
+
const PHASES = new Set([
|
|
34
|
+
"command",
|
|
35
|
+
"Rails prepare",
|
|
36
|
+
...Object.values(COMMAND_PHASES),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** Data descriptors avoid invoking an unknown error's getters while retaining bounded native buffers. */
|
|
40
|
+
export function mysqlErrorData(error, name) {
|
|
41
|
+
return error !== null &&
|
|
42
|
+
(typeof error === "object" || typeof error === "function")
|
|
43
|
+
? Object.getOwnPropertyDescriptor(error, name)?.value
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function captureFingerprint(value) {
|
|
48
|
+
return Buffer.isBuffer(value) && value.length <= 3145728
|
|
49
|
+
? Object.freeze({
|
|
50
|
+
bytes: value.length,
|
|
51
|
+
sha256: createHash("sha256").update(value).digest("hex"),
|
|
52
|
+
})
|
|
53
|
+
: null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Only typed status and hashes leave a native failure; its original capture stays in memory. */
|
|
57
|
+
export function mysqlObservation(error, phase) {
|
|
58
|
+
required(PHASES.has(phase), "MySQL native diagnostic phase differs");
|
|
59
|
+
const code = mysqlErrorData(error, "code");
|
|
60
|
+
const native = publicFailure(error);
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
phase,
|
|
63
|
+
status: Number.isInteger(code) && code >= 0 && code <= 255 ? code : null,
|
|
64
|
+
signal: /\bsignal=(SIG[A-Z]+)\b/.exec(native)?.[1] ?? null,
|
|
65
|
+
native,
|
|
66
|
+
stdout: captureFingerprint(mysqlErrorData(error, "stdout")),
|
|
67
|
+
stderr: captureFingerprint(mysqlErrorData(error, "stderr")),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Native failure status is useful; SQL, credentials, capture and argv are never diagnostics. */
|
|
72
|
+
export function mysqlFailure(error, stage, phase = stage) {
|
|
73
|
+
required(
|
|
74
|
+
stage === "command" || stage === "Rails prepare",
|
|
75
|
+
"MySQL native diagnostic stage differs"
|
|
76
|
+
);
|
|
77
|
+
const observation = mysqlObservation(error, phase);
|
|
78
|
+
return Object.assign(
|
|
79
|
+
new Error(`npm updater: MySQL native ${stage} failed`, { cause: error }),
|
|
80
|
+
{
|
|
81
|
+
code: observation.status,
|
|
82
|
+
observation,
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ownedContainer(values, state) {
|
|
88
|
+
const [id, name, labels, imageId, running] = values;
|
|
89
|
+
required(
|
|
90
|
+
values.length === 7 &&
|
|
91
|
+
/^[a-f0-9]{64}$/.test(id) &&
|
|
92
|
+
name === `/${state.name}` &&
|
|
93
|
+
Object.keys(labels).join(",") === "lisa.npm.mysql" &&
|
|
94
|
+
labels["lisa.npm.mysql"] === state.nonce &&
|
|
95
|
+
imageId === state.imageId &&
|
|
96
|
+
(state.containerId === null || id === state.containerId) &&
|
|
97
|
+
typeof running === "boolean",
|
|
98
|
+
"MySQL container ownership differs"
|
|
99
|
+
);
|
|
100
|
+
return { id, running };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ownedMounts(mounts, storage) {
|
|
104
|
+
const expected = storage.files.map(file => ({
|
|
105
|
+
Type: "bind",
|
|
106
|
+
Source: file.file,
|
|
107
|
+
Destination: `/run/lisa-${file.name}`,
|
|
108
|
+
RW: false,
|
|
109
|
+
}));
|
|
110
|
+
required(
|
|
111
|
+
Array.isArray(mounts) &&
|
|
112
|
+
mounts.length === 4 &&
|
|
113
|
+
expected.every(want =>
|
|
114
|
+
mounts.some(actual =>
|
|
115
|
+
Object.entries(want).every(([key, value]) => actual[key] === value)
|
|
116
|
+
)
|
|
117
|
+
) &&
|
|
118
|
+
mounts.some(
|
|
119
|
+
mount =>
|
|
120
|
+
mount.Type === "tmpfs" && mount.Destination === "/var/lib/mysql"
|
|
121
|
+
),
|
|
122
|
+
"MySQL container mount ownership differs"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ownedPort(ports, state) {
|
|
127
|
+
const bindings = ports?.["3306/tcp"];
|
|
128
|
+
required(
|
|
129
|
+
ports &&
|
|
130
|
+
Object.keys(ports).every(
|
|
131
|
+
key => key === "3306/tcp" || ports[key] === null
|
|
132
|
+
) &&
|
|
133
|
+
Array.isArray(bindings) &&
|
|
134
|
+
bindings.length === 1 &&
|
|
135
|
+
bindings[0].HostIp === "127.0.0.1" &&
|
|
136
|
+
/^[1-9]\d{0,4}$/.test(bindings[0].HostPort) &&
|
|
137
|
+
Number(bindings[0].HostPort) <= 65535 &&
|
|
138
|
+
(state.port === null || state.port === bindings[0].HostPort),
|
|
139
|
+
"MySQL private loopback port differs"
|
|
140
|
+
);
|
|
141
|
+
state.port = bindings[0].HostPort;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function inspectContainer(call, target, state, storage, deadline) {
|
|
145
|
+
const result = await call(
|
|
146
|
+
["container", "inspect", target, "--format", FIELDS],
|
|
147
|
+
undefined,
|
|
148
|
+
[0, 1],
|
|
149
|
+
deadline
|
|
150
|
+
);
|
|
151
|
+
if (result.code === 1) {
|
|
152
|
+
required(
|
|
153
|
+
(result.stdout.length === 0 || result.stdout.equals(Buffer.from("\n"))) &&
|
|
154
|
+
result.stderr.toString().trim() ===
|
|
155
|
+
`Error response from daemon: No such container: ${target}`,
|
|
156
|
+
"MySQL container absence is unproved"
|
|
157
|
+
);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const values = fields(result.stdout);
|
|
161
|
+
const observed = ownedContainer(values, state);
|
|
162
|
+
ownedMounts(values[5], storage);
|
|
163
|
+
if (observed.running) ownedPort(values[6], state);
|
|
164
|
+
return observed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Every command rechecks immutable client, private storage and the same daemon/phase. */
|
|
168
|
+
export function createMysqlDaemon(docker, storage, state, deadline) {
|
|
169
|
+
const call = async (args, input, allowed = [0], limit = deadline) => {
|
|
170
|
+
storage.validate();
|
|
171
|
+
await assertDockerQualified(docker, limit);
|
|
172
|
+
try {
|
|
173
|
+
return await runProcess(docker.path, args, {
|
|
174
|
+
env: docker.env,
|
|
175
|
+
input,
|
|
176
|
+
allowed,
|
|
177
|
+
timeout: runtimeTime(limit, args[0] === "pull" ? 1800000 : 10000),
|
|
178
|
+
maximum: 262144,
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw mysqlFailure(error, "command", COMMAND_PHASES[args[0]]);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
call,
|
|
186
|
+
inspect: (target, limit = deadline) =>
|
|
187
|
+
inspectContainer(call, target, state, storage, limit),
|
|
188
|
+
async census(limit = deadline) {
|
|
189
|
+
const result = await call(
|
|
190
|
+
["ps", "-aq", "--no-trunc"],
|
|
191
|
+
undefined,
|
|
192
|
+
[0],
|
|
193
|
+
limit
|
|
194
|
+
);
|
|
195
|
+
const ids = result.stdout.toString().trim().split(/\s+/).filter(Boolean);
|
|
196
|
+
required(
|
|
197
|
+
ids.length <= 10000 && ids.every(id => /^[a-f0-9]{64}$/.test(id)),
|
|
198
|
+
"MySQL daemon census is invalid"
|
|
199
|
+
);
|
|
200
|
+
return ids;
|
|
201
|
+
},
|
|
202
|
+
async qualifyImage() {
|
|
203
|
+
await call(["pull", "--platform", "linux/amd64", state.image]);
|
|
204
|
+
const image = fields(
|
|
205
|
+
(
|
|
206
|
+
await call([
|
|
207
|
+
"image",
|
|
208
|
+
"inspect",
|
|
209
|
+
state.image,
|
|
210
|
+
"--format",
|
|
211
|
+
"{{json .Id}}\t{{json .Os}}\t{{json .Architecture}}\t{{json .RepoDigests}}",
|
|
212
|
+
])
|
|
213
|
+
).stdout
|
|
214
|
+
);
|
|
215
|
+
required(
|
|
216
|
+
image.length === 4 &&
|
|
217
|
+
/^sha256:[a-f0-9]{64}$/.test(image[0]) &&
|
|
218
|
+
image[1] === "linux" &&
|
|
219
|
+
image[2] === "amd64" &&
|
|
220
|
+
Array.isArray(image[3]) &&
|
|
221
|
+
image[3].includes(state.image),
|
|
222
|
+
"MySQL immutable image platform differs"
|
|
223
|
+
);
|
|
224
|
+
state.imageId = image[0];
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Pending allocation, owned mutations and cleanup share the original absolute phase. */
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { required } from "./npm-update-contract.mjs";
|
|
7
|
+
import {
|
|
8
|
+
runtimeTime,
|
|
9
|
+
assertDockerQualified,
|
|
10
|
+
} from "./npm-update-rails-tool-identity.mjs";
|
|
11
|
+
import {
|
|
12
|
+
createMysqlStorage,
|
|
13
|
+
assertMysqlRoot,
|
|
14
|
+
} from "./npm-update-rails-mysql-storage.mjs";
|
|
15
|
+
import { createMysqlDaemon } from "./npm-update-rails-mysql-daemon.mjs";
|
|
16
|
+
export { assertMysqlRoot } from "./npm-update-rails-mysql-storage.mjs";
|
|
17
|
+
export { mysqlFailure } from "./npm-update-rails-mysql-daemon.mjs";
|
|
18
|
+
|
|
19
|
+
const IMAGE =
|
|
20
|
+
"mysql@sha256:80f4933e3835f9dc4d35a28ec500d7986cb4414e6c6821c5461239cb7beb8995";
|
|
21
|
+
|
|
22
|
+
async function allocate(daemon, storage, state) {
|
|
23
|
+
await daemon.qualifyImage();
|
|
24
|
+
const mounts = storage.files.flatMap(file => [
|
|
25
|
+
"--mount",
|
|
26
|
+
`type=bind,src=${file.file},dst=/run/lisa-${file.name},readonly`,
|
|
27
|
+
]);
|
|
28
|
+
state.pending = true;
|
|
29
|
+
const created = await daemon.call([
|
|
30
|
+
"create",
|
|
31
|
+
"--platform",
|
|
32
|
+
"linux/amd64",
|
|
33
|
+
"--name",
|
|
34
|
+
state.name,
|
|
35
|
+
"--label",
|
|
36
|
+
`lisa.npm.mysql=${state.nonce}`,
|
|
37
|
+
"--publish",
|
|
38
|
+
"127.0.0.1::3306",
|
|
39
|
+
...mounts,
|
|
40
|
+
"--mount",
|
|
41
|
+
"type=tmpfs,dst=/var/lib/mysql,tmpfs-mode=0700",
|
|
42
|
+
"--env",
|
|
43
|
+
"MYSQL_ROOT_PASSWORD_FILE=/run/lisa-root-password",
|
|
44
|
+
IMAGE,
|
|
45
|
+
"--partial-revokes=ON",
|
|
46
|
+
]);
|
|
47
|
+
const id = created.stdout.toString().trim();
|
|
48
|
+
required(/^[a-f0-9]{64}$/.test(id), "MySQL native create identity malformed");
|
|
49
|
+
state.containerId = id;
|
|
50
|
+
state.pending = false;
|
|
51
|
+
await daemon.inspect(id);
|
|
52
|
+
await daemon.call(["start", id]);
|
|
53
|
+
required(
|
|
54
|
+
(await daemon.inspect(id))?.running === true,
|
|
55
|
+
"MySQL owned container is not running"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function closeResource(daemon, storage, state, before, deadline) {
|
|
60
|
+
const cleanupDeadline = Math.min(deadline, Date.now() + 10000);
|
|
61
|
+
runtimeTime(cleanupDeadline);
|
|
62
|
+
const observed =
|
|
63
|
+
state.pending || state.containerId
|
|
64
|
+
? await daemon.inspect(state.containerId ?? state.name, cleanupDeadline)
|
|
65
|
+
: null;
|
|
66
|
+
if (observed) {
|
|
67
|
+
if (observed.running)
|
|
68
|
+
await daemon.call(
|
|
69
|
+
["stop", "--time", "1", observed.id],
|
|
70
|
+
undefined,
|
|
71
|
+
[0],
|
|
72
|
+
cleanupDeadline
|
|
73
|
+
);
|
|
74
|
+
const stopped = await daemon.inspect(observed.id, cleanupDeadline);
|
|
75
|
+
required(stopped && !stopped.running, "MySQL owned container did not stop");
|
|
76
|
+
await daemon.call(
|
|
77
|
+
["container", "rm", stopped.id],
|
|
78
|
+
undefined,
|
|
79
|
+
[0],
|
|
80
|
+
cleanupDeadline
|
|
81
|
+
);
|
|
82
|
+
required(
|
|
83
|
+
(await daemon.inspect(stopped.id, cleanupDeadline)) === null &&
|
|
84
|
+
(await daemon.inspect(state.name, cleanupDeadline)) === null,
|
|
85
|
+
"MySQL owned container remains"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const after = await daemon.census(cleanupDeadline);
|
|
89
|
+
required(
|
|
90
|
+
before.every(id => after.includes(id)),
|
|
91
|
+
"MySQL foreign daemon census changed"
|
|
92
|
+
);
|
|
93
|
+
state.foreignPreserved = true;
|
|
94
|
+
storage.remove();
|
|
95
|
+
state.closed = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One pending authority recovers lost native replies; cleanup failure never hides opening failure. */
|
|
99
|
+
export async function openMysqlResource({
|
|
100
|
+
root,
|
|
101
|
+
deadline,
|
|
102
|
+
docker,
|
|
103
|
+
user,
|
|
104
|
+
password,
|
|
105
|
+
}) {
|
|
106
|
+
required(
|
|
107
|
+
/^lisa_[a-f0-9]{24}$/.test(user) && /^[a-f0-9]{64}$/.test(password),
|
|
108
|
+
"MySQL synthetic account identity differs"
|
|
109
|
+
);
|
|
110
|
+
runtimeTime(deadline);
|
|
111
|
+
assertMysqlRoot(root);
|
|
112
|
+
await assertDockerQualified(docker, deadline);
|
|
113
|
+
const nonce = randomBytes(32).toString("hex");
|
|
114
|
+
const state = {
|
|
115
|
+
nonce,
|
|
116
|
+
name: `lisa-npm-mysql-${nonce}`,
|
|
117
|
+
image: IMAGE,
|
|
118
|
+
pending: false,
|
|
119
|
+
containerId: null,
|
|
120
|
+
imageId: null,
|
|
121
|
+
port: null,
|
|
122
|
+
closed: false,
|
|
123
|
+
foreignPreserved: false,
|
|
124
|
+
};
|
|
125
|
+
const storage = createMysqlStorage(
|
|
126
|
+
root,
|
|
127
|
+
{ nonce, name: state.name, image: IMAGE },
|
|
128
|
+
user,
|
|
129
|
+
password
|
|
130
|
+
);
|
|
131
|
+
const daemon = createMysqlDaemon(docker, storage, state, deadline);
|
|
132
|
+
let before = [];
|
|
133
|
+
let closing;
|
|
134
|
+
const close = () =>
|
|
135
|
+
(closing ??= closeResource(daemon, storage, state, before, deadline));
|
|
136
|
+
try {
|
|
137
|
+
before = await daemon.census();
|
|
138
|
+
await allocate(daemon, storage, state);
|
|
139
|
+
return {
|
|
140
|
+
state,
|
|
141
|
+
close,
|
|
142
|
+
async query(sql, application = false) {
|
|
143
|
+
required(
|
|
144
|
+
!state.closed &&
|
|
145
|
+
(await daemon.inspect(state.containerId))?.running === true,
|
|
146
|
+
"MySQL query ownership is not running"
|
|
147
|
+
);
|
|
148
|
+
const result = await daemon.call(
|
|
149
|
+
[
|
|
150
|
+
"exec",
|
|
151
|
+
"-i",
|
|
152
|
+
state.containerId,
|
|
153
|
+
"mysql",
|
|
154
|
+
`--defaults-extra-file=/run/lisa-${application ? "app" : "admin"}.cnf`,
|
|
155
|
+
"--batch",
|
|
156
|
+
"--raw",
|
|
157
|
+
"--skip-column-names",
|
|
158
|
+
],
|
|
159
|
+
sql
|
|
160
|
+
);
|
|
161
|
+
return result.stdout.toString();
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
try {
|
|
166
|
+
await close();
|
|
167
|
+
} catch (cleanup) {
|
|
168
|
+
throw new AggregateError(
|
|
169
|
+
[error, cleanup],
|
|
170
|
+
"MySQL open and owned cleanup failed",
|
|
171
|
+
{ cause: error }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Private file and inode authority is separate from the Docker mutation protocol. */
|
|
5
|
+
import {
|
|
6
|
+
lstatSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
openSync,
|
|
11
|
+
fstatSync,
|
|
12
|
+
closeSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
rmdirSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join, isAbsolute } from "node:path";
|
|
18
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
19
|
+
import { required } from "./npm-update-contract.mjs";
|
|
20
|
+
import { readBytes } from "./npm-update-process-core.mjs";
|
|
21
|
+
import { canonicalJson } from "./automation-provenance-contract.mjs";
|
|
22
|
+
|
|
23
|
+
const digest = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
24
|
+
|
|
25
|
+
/** Aliases, public storage and mount syntax cannot become allocation authority. */
|
|
26
|
+
export function assertMysqlRoot(root) {
|
|
27
|
+
const st = lstatSync(root);
|
|
28
|
+
required(
|
|
29
|
+
isAbsolute(root) &&
|
|
30
|
+
realpathSync(root) === root &&
|
|
31
|
+
!/[\n\r,]/.test(root) &&
|
|
32
|
+
st.isDirectory() &&
|
|
33
|
+
!st.isSymbolicLink() &&
|
|
34
|
+
st.uid === process.getuid() &&
|
|
35
|
+
(st.mode & 0o077) === 0,
|
|
36
|
+
"MySQL root is aliased or not private owned storage"
|
|
37
|
+
);
|
|
38
|
+
return { dev: st.dev, ino: st.ino };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Credentials never enter argv, Docker environment values or exported diagnostics. */
|
|
42
|
+
function privateFiles(directory, user, password, created) {
|
|
43
|
+
const rootPassword = randomBytes(32).toString("hex");
|
|
44
|
+
const values = {
|
|
45
|
+
"root-password": rootPassword,
|
|
46
|
+
"admin.cnf": `[client]\nuser=root\npassword=${rootPassword}\nhost=127.0.0.1\nprotocol=TCP\nget-server-public-key=1\n`,
|
|
47
|
+
"app.cnf": `[client]\nuser=${user}\npassword=${password}\nhost=127.0.0.1\nprotocol=TCP\nget-server-public-key=1\n`,
|
|
48
|
+
};
|
|
49
|
+
return Object.entries(values).map(([name, value]) => {
|
|
50
|
+
const file = join(directory, name);
|
|
51
|
+
return { name, ...privateWrite(file, value, created) };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Exclusive descriptor ownership is recorded before a write can fail partially. */
|
|
56
|
+
function privateWrite(file, value, created) {
|
|
57
|
+
const fd = openSync(file, "wx", 0o600);
|
|
58
|
+
try {
|
|
59
|
+
const st = fstatSync(fd);
|
|
60
|
+
const owned = { file, dev: st.dev, ino: st.ino };
|
|
61
|
+
created.push(owned);
|
|
62
|
+
writeFileSync(fd, value);
|
|
63
|
+
return { ...owned, sha256: digest(Buffer.from(value)) };
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Only exclusively created private inodes can be removed after construction failure. */
|
|
70
|
+
function removeConstruction(root, rootId, directory, dirId, created) {
|
|
71
|
+
const nowRoot = assertMysqlRoot(root);
|
|
72
|
+
const nowDir = assertMysqlRoot(directory);
|
|
73
|
+
const inDirectory = created.filter(file =>
|
|
74
|
+
file.file.startsWith(`${directory}/`)
|
|
75
|
+
);
|
|
76
|
+
required(
|
|
77
|
+
nowRoot.dev === rootId.dev &&
|
|
78
|
+
nowRoot.ino === rootId.ino &&
|
|
79
|
+
nowDir.dev === dirId.dev &&
|
|
80
|
+
nowDir.ino === dirId.ino &&
|
|
81
|
+
readdirSync(directory).sort().join(",") ===
|
|
82
|
+
inDirectory
|
|
83
|
+
.map(file => file.file.slice(directory.length + 1))
|
|
84
|
+
.sort()
|
|
85
|
+
.join(","),
|
|
86
|
+
"MySQL partial storage ownership changed"
|
|
87
|
+
);
|
|
88
|
+
for (const file of created) {
|
|
89
|
+
const st = lstatSync(file.file);
|
|
90
|
+
required(
|
|
91
|
+
st.isFile() &&
|
|
92
|
+
!st.isSymbolicLink() &&
|
|
93
|
+
st.nlink === 1 &&
|
|
94
|
+
st.uid === process.getuid() &&
|
|
95
|
+
(st.mode & 0o077) === 0 &&
|
|
96
|
+
st.dev === file.dev &&
|
|
97
|
+
st.ino === file.ino,
|
|
98
|
+
"MySQL partial file ownership changed"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
for (const file of inDirectory) unlinkSync(file.file);
|
|
102
|
+
rmdirSync(directory);
|
|
103
|
+
for (const file of created.filter(file => !inDirectory.includes(file)))
|
|
104
|
+
unlinkSync(file.file);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Re-read immutable private bytes and directory census before every daemon operation. */
|
|
108
|
+
function validateStorage(storage) {
|
|
109
|
+
const {
|
|
110
|
+
root,
|
|
111
|
+
directory,
|
|
112
|
+
rootId,
|
|
113
|
+
dirId,
|
|
114
|
+
registration,
|
|
115
|
+
registrationBytes,
|
|
116
|
+
files,
|
|
117
|
+
} = storage;
|
|
118
|
+
const nowRoot = assertMysqlRoot(root);
|
|
119
|
+
const nowDir = assertMysqlRoot(directory);
|
|
120
|
+
required(
|
|
121
|
+
nowRoot.dev === rootId.dev &&
|
|
122
|
+
nowRoot.ino === rootId.ino &&
|
|
123
|
+
nowDir.dev === dirId.dev &&
|
|
124
|
+
nowDir.ino === dirId.ino &&
|
|
125
|
+
readBytes(registration).equals(registrationBytes),
|
|
126
|
+
"MySQL private ownership changed"
|
|
127
|
+
);
|
|
128
|
+
required(
|
|
129
|
+
readdirSync(directory).sort().join(",") ===
|
|
130
|
+
files
|
|
131
|
+
.map(file => file.name)
|
|
132
|
+
.sort()
|
|
133
|
+
.join(","),
|
|
134
|
+
"MySQL private ownership contains foreign data"
|
|
135
|
+
);
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const st = lstatSync(file.file);
|
|
138
|
+
required(
|
|
139
|
+
st.dev === file.dev &&
|
|
140
|
+
st.ino === file.ino &&
|
|
141
|
+
digest(readBytes(file.file)) === file.sha256,
|
|
142
|
+
"MySQL private file ownership changed"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A fixed registration prevents the enclosing updater from deleting outstanding resource authority. */
|
|
148
|
+
export function createMysqlStorage(root, identity, user, password) {
|
|
149
|
+
const rootId = assertMysqlRoot(root);
|
|
150
|
+
const directory = join(root, `rails-mysql-${identity.nonce}`);
|
|
151
|
+
mkdirSync(directory, { mode: 0o700 });
|
|
152
|
+
const dirId = assertMysqlRoot(directory);
|
|
153
|
+
const created = [];
|
|
154
|
+
let files;
|
|
155
|
+
const registration = join(
|
|
156
|
+
root,
|
|
157
|
+
`namespace-${identity.nonce.slice(0, 16)}.json`
|
|
158
|
+
);
|
|
159
|
+
try {
|
|
160
|
+
privateWrite(
|
|
161
|
+
registration,
|
|
162
|
+
`${canonicalJson({ version: 1, kind: "rails-mysql", ...identity, directory })}\n`,
|
|
163
|
+
created
|
|
164
|
+
);
|
|
165
|
+
files = privateFiles(directory, user, password, created);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
try {
|
|
168
|
+
removeConstruction(root, rootId, directory, dirId, created);
|
|
169
|
+
} catch (cleanup) {
|
|
170
|
+
throw new AggregateError(
|
|
171
|
+
[error, cleanup],
|
|
172
|
+
"MySQL partial storage cleanup refused",
|
|
173
|
+
{ cause: error }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
const storage = {
|
|
179
|
+
root,
|
|
180
|
+
rootId,
|
|
181
|
+
directory,
|
|
182
|
+
dirId,
|
|
183
|
+
files,
|
|
184
|
+
registration,
|
|
185
|
+
registrationBytes: readBytes(registration),
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
files,
|
|
189
|
+
validate: () => validateStorage(storage),
|
|
190
|
+
remove() {
|
|
191
|
+
validateStorage(storage);
|
|
192
|
+
for (const file of files) unlinkSync(file.file);
|
|
193
|
+
rmdirSync(directory);
|
|
194
|
+
required(
|
|
195
|
+
!readdirSync(root).includes(directory.slice(root.length + 1)),
|
|
196
|
+
"MySQL private directory remains"
|
|
197
|
+
);
|
|
198
|
+
unlinkSync(registration);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|