@ouro.bot/cli 0.1.0-alpha.970 → 0.1.0-alpha.972
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 +11 -0
- package/changelog.json +12 -0
- package/deploy/unraid/INSTALL.md +64 -0
- package/deploy/unraid/docker-man-template-transaction.mjs +20 -4
- package/deploy/unraid/install.sh +274 -0
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +5 -2
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +126 -44
- package/deploy/unraid/sanctuary.ouro/agent.json +4 -4
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +2 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +18 -12
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +3 -5
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-exec.js +16 -9
- package/dist/heart/daemon/cli-help.js +1 -1
- package/dist/heart/daemon/cli-parse.js +14 -4
- package/dist/heart/daemon/sanctuary-authority-epoch.js +21 -11
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +53 -11
- package/npm-shrinkwrap.json +334 -182
- package/package.json +3 -2
|
@@ -265,7 +265,7 @@ function usage() {
|
|
|
265
265
|
" ouro vault recover [--agent <name>] --from <json> [--from <json>] [--email <email>] [--server <url>] [--store <store>]",
|
|
266
266
|
" ouro vault unlock [--agent <name>] [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
|
|
267
267
|
" ouro vault status [--agent <name>] [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
|
|
268
|
-
" ouro vault config set [--agent <name>] --key <path> [--value <value>] [--scope agent|machine]",
|
|
268
|
+
" ouro vault config set [--agent <name>] --key <path> [--value <value> | --value-file <path>] [--scope agent|machine]",
|
|
269
269
|
" ouro vault config status [--agent <name>] [--scope agent|machine|all]",
|
|
270
270
|
" ouro vault item set [--agent <name>] --item <path> --secret-field <name> [--public-field <key=value>] [--note <text>]",
|
|
271
271
|
" ouro vault item status [--agent <name>] --item <path>",
|
|
@@ -1173,9 +1173,15 @@ function parseVaultConfigCommand(args) {
|
|
|
1173
1173
|
const { agent, rest } = extractAgentFlag(args.slice(1));
|
|
1174
1174
|
let key;
|
|
1175
1175
|
let value;
|
|
1176
|
+
let valueFile;
|
|
1176
1177
|
let scope;
|
|
1177
1178
|
for (let i = 0; i < rest.length; i += 1) {
|
|
1178
1179
|
const token = rest[i];
|
|
1180
|
+
if (token === "--value-file") {
|
|
1181
|
+
valueFile = rest[i + 1] ?? "";
|
|
1182
|
+
i += 1;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1179
1185
|
if (token === "--key") {
|
|
1180
1186
|
key = rest[i + 1];
|
|
1181
1187
|
i += 1;
|
|
@@ -1201,7 +1207,7 @@ function parseVaultConfigCommand(args) {
|
|
|
1201
1207
|
throw new Error("Usage: ouro vault config set [--agent <name>] --key <path> [--value <value>] OR ouro vault config status [--agent <name>]");
|
|
1202
1208
|
}
|
|
1203
1209
|
if (sub === "status") {
|
|
1204
|
-
if (key || value) {
|
|
1210
|
+
if (key || value || valueFile) {
|
|
1205
1211
|
throw new Error("Usage: ouro vault config status [--agent <name>]");
|
|
1206
1212
|
}
|
|
1207
1213
|
return { kind: "vault.config.status", ...(agent ? { agent } : {}), ...(scope ? { scope } : {}) };
|
|
@@ -1209,9 +1215,13 @@ function parseVaultConfigCommand(args) {
|
|
|
1209
1215
|
if (scope === "all")
|
|
1210
1216
|
throw new Error("vault config --scope all is only valid for status");
|
|
1211
1217
|
if (!key) {
|
|
1212
|
-
throw new Error("Usage: ouro vault config set [--agent <name>] --key <path> [--value <value>]");
|
|
1218
|
+
throw new Error("Usage: ouro vault config set [--agent <name>] --key <path> [--value <value> | --value-file <path>]");
|
|
1213
1219
|
}
|
|
1214
|
-
|
|
1220
|
+
if (value !== undefined && valueFile !== undefined)
|
|
1221
|
+
throw new Error("vault config set takes --value or --value-file, not both");
|
|
1222
|
+
if (valueFile !== undefined && !valueFile)
|
|
1223
|
+
throw new Error("vault config set --value-file needs a path (use /dev/stdin to pipe the value)");
|
|
1224
|
+
return { kind: "vault.config.set", ...(agent ? { agent } : {}), key, ...(value !== undefined ? { value } : {}), ...(valueFile !== undefined ? { valueFile } : {}), ...(scope ? { scope } : {}) };
|
|
1215
1225
|
}
|
|
1216
1226
|
function normalizeConnectTarget(value) {
|
|
1217
1227
|
if (!value)
|
|
@@ -81,9 +81,12 @@ function validate(value) {
|
|
|
81
81
|
|| typeof epoch.epochId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/u.test(epoch.epochId)
|
|
82
82
|
|| ![epoch.botId, epoch.ownerUserId, epoch.ownerChatId].every((id) => typeof id === "string" && /^[1-9][0-9]*$/u.test(id)) || epoch.ownerUserId !== epoch.ownerChatId
|
|
83
83
|
|| !Number.isSafeInteger(epoch.predecessorCursor) || epoch.predecessorCursor < 0
|
|
84
|
-
|| ![epoch.packageDigest, epoch.tokenDigest, epoch.
|
|
84
|
+
|| ![epoch.packageDigest, epoch.tokenDigest, epoch.publicKeyDigest].every((value) => typeof value === "string" && DIGEST.test(value))
|
|
85
|
+
|| (epoch.previousTokenDigest === null
|
|
86
|
+
? epoch.revokedTokenStatus !== null || epoch.predecessorCursor !== 0
|
|
87
|
+
: typeof epoch.previousTokenDigest !== "string" || !DIGEST.test(epoch.previousTokenDigest) || epoch.revokedTokenStatus !== 401)
|
|
85
88
|
|| epoch.tokenDigest === epoch.previousTokenDigest || typeof epoch.tokenPath !== "string" || !path.isAbsolute(epoch.tokenPath)
|
|
86
|
-
|| typeof epoch.publicKeyPem !== "string"
|
|
89
|
+
|| typeof epoch.publicKeyPem !== "string"
|
|
87
90
|
|| typeof epoch.createdAt !== "string" || !Number.isFinite(Date.parse(epoch.createdAt)) || new Date(epoch.createdAt).toISOString() !== epoch.createdAt
|
|
88
91
|
|| (epoch.state === "prepared" ? epoch.terminalCursor !== null : !Number.isSafeInteger(epoch.terminalCursor) || epoch.terminalCursor < epoch.predecessorCursor))
|
|
89
92
|
throw new Error("Sanctuary authority epoch is invalid");
|
|
@@ -154,19 +157,26 @@ async function prepareSanctuaryAuthorityEpoch(input, options) {
|
|
|
154
157
|
|| !Number.isSafeInteger(input.predecessorCursor) || input.predecessorCursor < 0 || !DIGEST.test(input.packageDigest))
|
|
155
158
|
throw new Error("Sanctuary authority epoch input is invalid");
|
|
156
159
|
const token = privateBytes(input.tokenPath, options).toString("utf8").trim();
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
// A fresh install has no predecessor token to prove revoked, and therefore no cursor to carry.
|
|
161
|
+
const previousToken = input.previousTokenPath === null ? null : privateBytes(input.previousTokenPath, options).toString("utf8").trim();
|
|
162
|
+
if (previousToken === null && input.predecessorCursor !== 0)
|
|
163
|
+
throw new Error("Sanctuary authority epoch input is invalid");
|
|
164
|
+
if (!TOKEN.test(token) || token.split(":")[0] !== input.botId
|
|
165
|
+
|| (previousToken !== null && (!TOKEN.test(previousToken) || previousToken.split(":")[0] !== input.botId || token === previousToken)))
|
|
159
166
|
throw new Error("Sanctuary authority requires a fresh same-bot token");
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
167
|
+
if (previousToken !== null) {
|
|
168
|
+
const previous = await options.probe(previousToken);
|
|
169
|
+
if (previous.status !== 401 || previous.botId !== null)
|
|
170
|
+
throw new Error("Sanctuary previous Telegram token is not proven revoked");
|
|
171
|
+
}
|
|
163
172
|
const current = await options.probe(token);
|
|
164
173
|
if (current.status !== 200 || current.botId !== input.botId)
|
|
165
174
|
throw new Error("Sanctuary new Telegram token identity is invalid");
|
|
166
175
|
const epochPath = path.join(input.root, "epoch.json");
|
|
167
176
|
const keyPath = path.join(input.root, "issuer.pem");
|
|
168
177
|
const intentPath = path.join(input.root, "issuer-intent.json");
|
|
169
|
-
const
|
|
178
|
+
const previousTokenDigest = previousToken === null ? null : sha(previousToken);
|
|
179
|
+
const inputDigest = sha(JSON.stringify({ ...input, tokenDigest: sha(token), previousTokenDigest }));
|
|
170
180
|
return (0, session_transaction_1.withImmediateSessionTurnLease)(epochPath, (lease) => {
|
|
171
181
|
const transaction = (0, session_transaction_1.readSessionTransaction)(epochPath, lease);
|
|
172
182
|
let existing = false;
|
|
@@ -186,7 +196,7 @@ async function prepareSanctuaryAuthorityEpoch(input, options) {
|
|
|
186
196
|
if (epoch[key] !== input[key])
|
|
187
197
|
throw new Error("Sanctuary authority epoch input changed");
|
|
188
198
|
}
|
|
189
|
-
if (epoch.previousTokenDigest !==
|
|
199
|
+
if (epoch.previousTokenDigest !== previousTokenDigest)
|
|
190
200
|
throw new Error("Sanctuary authority predecessor token changed");
|
|
191
201
|
if (fs.existsSync(intentPath)) {
|
|
192
202
|
const intent = JSON.parse(privateBytes(intentPath, options).toString("utf8"));
|
|
@@ -218,8 +228,8 @@ async function prepareSanctuaryAuthorityEpoch(input, options) {
|
|
|
218
228
|
}
|
|
219
229
|
const epoch = validate({
|
|
220
230
|
schemaVersion: 1, state: "prepared", epochId: input.epochId, botId: input.botId, ownerUserId: input.ownerUserId, ownerChatId: input.ownerChatId,
|
|
221
|
-
predecessorCursor: input.predecessorCursor, packageDigest: input.packageDigest, tokenPath: input.tokenPath, tokenDigest: sha(token), previousTokenDigest
|
|
222
|
-
...issuer(input.root, options), revokedTokenStatus: 401, createdAt: intent.createdAt, terminalCursor: null,
|
|
231
|
+
predecessorCursor: input.predecessorCursor, packageDigest: input.packageDigest, tokenPath: input.tokenPath, tokenDigest: sha(token), previousTokenDigest,
|
|
232
|
+
...issuer(input.root, options), revokedTokenStatus: previousTokenDigest === null ? null : 401, createdAt: intent.createdAt, terminalCursor: null,
|
|
223
233
|
});
|
|
224
234
|
(0, session_transaction_1.writeSessionTransaction)(epochPath, epoch, { lease, expectedRevision: transaction.revision });
|
|
225
235
|
fs.unlinkSync(intentPath);
|
|
@@ -103,7 +103,10 @@ const EPOCH_RECORDS = ["migration.json", "stage.json", "configured.json"];
|
|
|
103
103
|
const digest = (value) => `sha256:${(0, node_crypto_1.createHash)("sha256").update(value).digest("hex")}`;
|
|
104
104
|
function validateRequest(value) {
|
|
105
105
|
const request = value;
|
|
106
|
-
|
|
106
|
+
const keys = "botId,epochId,nodeDigest,ownerChatId,ownerUserId,packageDigest,prlimitDigest,schemaVersion,setsidDigest,shellDigest";
|
|
107
|
+
if (!request || typeof request !== "object"
|
|
108
|
+
|| Object.keys(request).filter((key) => key !== "predecessorContract").sort().join(",") !== keys
|
|
109
|
+
|| (Object.hasOwn(request, "predecessorContract") && request.predecessorContract !== "none")
|
|
107
110
|
|| request.schemaVersion !== 1 || !/^[A-Za-z0-9_-]{1,128}$/u.test(request.epochId)
|
|
108
111
|
|| ![request.botId, request.ownerUserId, request.ownerChatId].every((id) => typeof id === "string" && /^[1-9][0-9]*$/u.test(id))
|
|
109
112
|
|| request.ownerUserId !== request.ownerChatId
|
|
@@ -128,17 +131,20 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
128
131
|
this.#socketGid = options.socketGroupId ?? 10001;
|
|
129
132
|
if (process.getuid() !== this.#uid || process.getgid() !== this.#gid)
|
|
130
133
|
throw new Error("Sanctuary root lifecycle requires root");
|
|
131
|
-
if (!DIGEST.test(transaction.targetImageId) || !DIGEST.test(transaction.rollbackImageId) || transaction.targetImageId === transaction.rollbackImageId)
|
|
132
|
-
throw new Error("Sanctuary root lifecycle image identity is invalid");
|
|
133
|
-
this.#transaction = { targetImageId: transaction.targetImageId, rollbackImageId: transaction.rollbackImageId };
|
|
134
134
|
this.#directory(ROOT);
|
|
135
135
|
this.#request = validateRequest(JSON.parse(this.#private(`${ROOT}/request.json`)));
|
|
136
|
+
const rollbackValid = transaction.rollbackImageId === null ? this.#fresh() : DIGEST.test(transaction.rollbackImageId);
|
|
137
|
+
if (!DIGEST.test(transaction.targetImageId) || !rollbackValid || transaction.targetImageId === transaction.rollbackImageId)
|
|
138
|
+
throw new Error("Sanctuary root lifecycle image identity is invalid");
|
|
139
|
+
this.#transaction = { targetImageId: transaction.targetImageId, rollbackImageId: transaction.rollbackImageId };
|
|
136
140
|
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_root_lifecycle_loaded", message: "Sanctuary root lifecycle identity loaded", meta: { epochId: this.#request.epochId, targetImageId: transaction.targetImageId } });
|
|
137
141
|
}
|
|
138
142
|
plan() {
|
|
139
143
|
const { epochId, botId, ownerUserId, ownerChatId, packageDigest } = this.#request;
|
|
140
|
-
return { schemaVersion: 1, epochId, botId, ownerUserId, ownerChatId, packageDigest, publicKeyDigest: null, predecessorContract: "canonical-pre-gateway", targetContract: "canonical-gateway" };
|
|
144
|
+
return { schemaVersion: 1, epochId, botId, ownerUserId, ownerChatId, packageDigest, publicKeyDigest: null, predecessorContract: this.#fresh() ? "none" : "canonical-pre-gateway", targetContract: "canonical-gateway" };
|
|
141
145
|
}
|
|
146
|
+
/** Whether this is a fresh install, which hands over nothing from a predecessor. */
|
|
147
|
+
#fresh() { return this.#request.predecessorContract === "none"; }
|
|
142
148
|
async boot() {
|
|
143
149
|
if (!fs.existsSync(this.#p(`${ROOT}/activation.json`)))
|
|
144
150
|
return false;
|
|
@@ -212,9 +218,15 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
212
218
|
"rollback:reconcile-executions": { read: () => this.#retiredProcess(), apply: () => this.#retireProcess() },
|
|
213
219
|
"rollback:stop-gateway": { read: () => this.#retiredProcess() && this.#pid() === null, apply: () => this.#stopGateway() },
|
|
214
220
|
"rollback:end-epoch": { read: () => this.#epoch().state === "retired", apply: () => this.#endEpoch() },
|
|
215
|
-
"rollback:restore-token-cursor":
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
"rollback:restore-token-cursor": this.#fresh()
|
|
222
|
+
? { read: () => this.#epoch().state === "retired", apply: () => this.#requireStopped() }
|
|
223
|
+
: { read: () => this.#restoredToken(), apply: () => this.#restoreToken() },
|
|
224
|
+
"rollback:restore-resident": this.#fresh()
|
|
225
|
+
? { read: () => this.#residentAbsent(), apply: () => this.#removeResident() }
|
|
226
|
+
: { read: () => this.#rollback().State.Running, apply: () => this.#restoreResident() },
|
|
227
|
+
"rollback:verify-rollback": this.#fresh()
|
|
228
|
+
? { read: () => fs.existsSync(this.#p(`${this.#epochRoot()}/rollback.json`)) && this.#freshRollbackClean(), apply: () => this.#verifyFreshRollback() }
|
|
229
|
+
: { read: () => this.#rollbackVerified(), apply: () => this.#verifyRollback() },
|
|
218
230
|
};
|
|
219
231
|
const effect = effects[step];
|
|
220
232
|
if (!effect)
|
|
@@ -248,6 +260,13 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
248
260
|
this.#migration();
|
|
249
261
|
return;
|
|
250
262
|
}
|
|
263
|
+
if (this.#fresh()) {
|
|
264
|
+
// A fresh install starts from nothing: an installed authority here means this is not one.
|
|
265
|
+
if (fs.existsSync(this.#p(`${ROOT}/active.json`)) || fs.existsSync(this.#p(`${ROOT}/activation.json`)))
|
|
266
|
+
throw new Error("Sanctuary fresh install found an installed authority");
|
|
267
|
+
this.#write(`${this.#epochRoot()}/migration.json`, JSON.stringify({ ...this.plan(), ...this.#transaction, predecessorCursor: 0 }));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
251
270
|
if (fs.existsSync(this.#p(`${ROOT}/active.json`))) {
|
|
252
271
|
const previous = JSON.parse(this.#private(`${ROOT}/active.json`));
|
|
253
272
|
const epoch = (0, sanctuary_authority_epoch_1.readSanctuaryAuthorityEpoch)(previous.epochRoot, { expectedUid: this.#uid, expectedGid: this.#gid });
|
|
@@ -430,7 +449,7 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
430
449
|
this.#write(`${this.#epochRoot()}/current-token`, token);
|
|
431
450
|
await (0, sanctuary_authority_epoch_1.prepareSanctuaryAuthorityEpoch)({
|
|
432
451
|
root: this.#p(this.#epochRoot()), epochId: this.#request.epochId, tokenPath: this.#p(`${this.#epochRoot()}/current-token`),
|
|
433
|
-
previousTokenPath: this.#p(`${this.#epochRoot()}/previous-token`), botId: this.#request.botId,
|
|
452
|
+
previousTokenPath: this.#fresh() ? null : this.#p(`${this.#epochRoot()}/previous-token`), botId: this.#request.botId,
|
|
434
453
|
ownerUserId: this.#request.ownerUserId, ownerChatId: this.#request.ownerChatId,
|
|
435
454
|
predecessorCursor: this.#migration().predecessorCursor, packageDigest: this.#request.packageDigest,
|
|
436
455
|
}, {
|
|
@@ -781,6 +800,29 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
781
800
|
this.#docker(["start", "ouro-butler"]);
|
|
782
801
|
await this.#wait(() => this.#rollback().State.Health?.Status === "healthy" && this.#rollback().State.Running);
|
|
783
802
|
}
|
|
803
|
+
// A fresh install rolls back to nothing: its token stays in root custody for a retry,
|
|
804
|
+
// and the resident it created is removed rather than restored.
|
|
805
|
+
#residentAbsent() {
|
|
806
|
+
return !this.#containers().some((container) => container.Name === "/ouro-butler");
|
|
807
|
+
}
|
|
808
|
+
#removeResident() {
|
|
809
|
+
this.#assertNoForeignPoller();
|
|
810
|
+
if (this.#epoch().state !== "retired" || this.#pid() !== null)
|
|
811
|
+
throw new Error("Sanctuary epoch must retire before the fresh resident is removed");
|
|
812
|
+
const resident = this.#containers().find((container) => container.Name === "/ouro-butler");
|
|
813
|
+
if (resident && resident.Image !== this.#transaction.targetImageId)
|
|
814
|
+
throw new Error("Sanctuary resident image changed");
|
|
815
|
+
if (resident)
|
|
816
|
+
this.#docker(["rm", "--force", "ouro-butler"]);
|
|
817
|
+
}
|
|
818
|
+
#freshRollbackClean() {
|
|
819
|
+
return this.#epoch().state === "retired" && this.#pid() === null && this.#residentAbsent();
|
|
820
|
+
}
|
|
821
|
+
#verifyFreshRollback() {
|
|
822
|
+
if (!this.#freshRollbackClean())
|
|
823
|
+
throw new Error("Sanctuary rollback readback failed");
|
|
824
|
+
this.#write(`${this.#epochRoot()}/rollback.json`, JSON.stringify({ epochId: this.#request.epochId, state: "retired" }));
|
|
825
|
+
}
|
|
784
826
|
#rollbackVerified() {
|
|
785
827
|
return fs.existsSync(this.#p(`${this.#epochRoot()}/rollback.json`)) && this.#restoredToken() && this.#pid() === null && this.#rollback().State.Running && this.#rollback().State.Health?.Status === "healthy";
|
|
786
828
|
}
|
|
@@ -889,7 +931,7 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
889
931
|
|| !journal.completed.every((step) => exports.SANCTUARY_UPGRADE_STEPS.includes(step))
|
|
890
932
|
|| (journal.rollingBack !== undefined && journal.rollingBack !== true)
|
|
891
933
|
|| !sides.every((side) => side && DIGEST.test(side.imageId) && DIGEST.test(side.packageDigest) && IMAGE_REFERENCE.test(side.imageReference))
|
|
892
|
-
|| !DIGEST.test(journal.from.rollbackImageId))
|
|
934
|
+
|| !(journal.from.rollbackImageId === null ? this.#fresh() : DIGEST.test(journal.from.rollbackImageId)))
|
|
893
935
|
throw new Error("Sanctuary upgrade journal is invalid");
|
|
894
936
|
return journal;
|
|
895
937
|
}
|
|
@@ -901,7 +943,7 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
901
943
|
if (this.#docker(["image", "inspect", "--format", "{{.Id}}", input.imageReference]).trim() !== input.targetImageId)
|
|
902
944
|
throw new Error("Sanctuary upgrade image identity does not match its reference");
|
|
903
945
|
const request = validateRequest(JSON.parse(this.#private(INCOMING_REQUEST)));
|
|
904
|
-
if (["epochId", "botId", "ownerUserId", "ownerChatId"].some((key) => request[key] !== this.#request[key]))
|
|
946
|
+
if (["epochId", "botId", "ownerUserId", "ownerChatId", "predecessorContract"].some((key) => request[key] !== this.#request[key]))
|
|
905
947
|
throw new Error("Sanctuary upgrade would change the authority identity");
|
|
906
948
|
if (request.packageDigest === this.#request.packageDigest)
|
|
907
949
|
throw new Error("Sanctuary upgrade package is already installed");
|