@ouro.bot/cli 0.1.0-alpha.736 → 0.1.0-alpha.738
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.json +15 -0
- package/deploy/unraid/Dockerfile +17 -1
- package/deploy/unraid/README.txt +20 -2
- package/deploy/unraid/sanctuary-acceptance-contract.json +2 -2
- package/deploy/unraid/sanctuary-deployment-target.mjs +6 -2
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +23 -4
- package/deploy/unraid/sanctuary-unit16-run.sh +40 -22
- package/dist/heart/daemon/container-credential-bootstrap.js +69 -4
- package/npm-shrinkwrap.json +1634 -41
- package/package.json +3 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.738",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Allow the path-locked Sanctuary legacy adoption to discard only the exact obsolete MiniMax provider record in memory while preserving the original envelope and keeping canonical credential validation strict.",
|
|
8
|
+
"Validate the complete provider discard allowlist and every projected runtime message before the first vault write, with recoverable retry semantics and counts-only audit evidence."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.737",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Ship the integrity-locked Bitwarden CLI in the non-root Unraid runtime so fresh Sanctuary vault access never depends on an impossible runtime install.",
|
|
15
|
+
"Add a closed final-production acceptance profile with isolated evidence roots while preserving the existing fixed staging path.",
|
|
16
|
+
"Verify the packed Bitwarden CLI as the non-root runtime user with isolated temporary state, zero stderr, same-layer cleanup, and a read-only constrained-tmpfs image probe."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.736",
|
|
6
21
|
"changes": [
|
package/deploy/unraid/Dockerfile
CHANGED
|
@@ -12,7 +12,9 @@ RUN apt-get update \
|
|
|
12
12
|
|
|
13
13
|
WORKDIR /opt/ouro
|
|
14
14
|
COPY package.json npm-shrinkwrap.json ./
|
|
15
|
-
RUN npm ci --omit=dev --legacy-peer-deps --workspaces=false --no-audit --no-fund
|
|
15
|
+
RUN npm ci --omit=dev --legacy-peer-deps --workspaces=false --no-audit --no-fund \
|
|
16
|
+
&& test -x /opt/ouro/node_modules/.bin/bw \
|
|
17
|
+
&& ln -s /opt/ouro/node_modules/.bin/bw /usr/local/bin/bw
|
|
16
18
|
COPY dist ./dist
|
|
17
19
|
COPY assets ./assets
|
|
18
20
|
COPY skills ./skills
|
|
@@ -23,5 +25,19 @@ RUN chown -R 10001:10001 /opt/ouro /home/ouro
|
|
|
23
25
|
|
|
24
26
|
USER 10001:10001
|
|
25
27
|
ENV HOME=/home/ouro
|
|
28
|
+
RUN BW_VERIFY_ROOT="$(mktemp -d)" \
|
|
29
|
+
&& trap 'rm -rf -- "$BW_VERIFY_ROOT"' 0 \
|
|
30
|
+
&& mkdir "$BW_VERIFY_ROOT/appdata" \
|
|
31
|
+
&& printf '%s' '{}' >"$BW_VERIFY_ROOT/appdata/data.json" \
|
|
32
|
+
&& test ! -e "/home/ouro/.config/Bitwarden CLI" \
|
|
33
|
+
&& test "$(id -u)" = "10001" \
|
|
34
|
+
&& test "$(id -g)" = "10001" \
|
|
35
|
+
&& command -v bw >/dev/null \
|
|
36
|
+
&& BW_VERSION="$(BITWARDENCLI_APPDATA_DIR="$BW_VERIFY_ROOT/appdata" bw --version 2>"$BW_VERIFY_ROOT/stderr")" \
|
|
37
|
+
&& test ! -s "$BW_VERIFY_ROOT/stderr" \
|
|
38
|
+
&& test "$BW_VERSION" = "2026.8.0" \
|
|
39
|
+
&& rm -rf -- "$BW_VERIFY_ROOT" \
|
|
40
|
+
&& trap - 0 \
|
|
41
|
+
&& test ! -e "/home/ouro/.config/Bitwarden CLI"
|
|
26
42
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 CMD ["node","/opt/ouro/dist/heart/daemon/container-healthcheck.js","--agent","sanctuary"]
|
|
27
43
|
ENTRYPOINT ["node","/opt/ouro/dist/heart/daemon/daemon-entry.js"]
|
package/deploy/unraid/README.txt
CHANGED
|
@@ -713,12 +713,18 @@ local unlock: available
|
|
|
713
713
|
machineIdMigration: {
|
|
714
714
|
sourceMachineId: process.argv[1],
|
|
715
715
|
targetMachineId: process.argv[2],
|
|
716
|
+
discardProviderCredentialRecords: { providers: ["minimax"] },
|
|
716
717
|
},
|
|
717
718
|
}).then(
|
|
718
719
|
() => process.exit(0),
|
|
719
720
|
() => { process.stderr.write("credential bootstrap failed\n"); process.exit(1) },
|
|
720
721
|
)
|
|
721
722
|
' "$BOOTSTRAP_SOURCE_MACHINE_ID" "$BOOTSTRAP_TARGET_MACHINE_ID" || return $?
|
|
723
|
+
# The path-locked Sanctuary migration projects the exact obsolete Minimax
|
|
724
|
+
# provider record out of memory before strict validation and persistence.
|
|
725
|
+
# The original legacy envelope remains byte-for-byte unchanged and is the
|
|
726
|
+
# sole digest authority; current GLM and Gemini credentials are established
|
|
727
|
+
# only by the separate hidden-input authentication commands below.
|
|
722
728
|
! docker container inspect ouro-butler-credential-bootstrap >/dev/null 2>&1 || return 1
|
|
723
729
|
test ! -e "$BOOTSTRAP_CREDENTIAL_SOURCE" || return $?
|
|
724
730
|
test ! -e "$BOOTSTRAP_CREDENTIAL_CLAIM" || return $?
|
|
@@ -1386,8 +1392,10 @@ Packaged deployment-target containment gates:
|
|
|
1386
1392
|
types (including datagrams) fail closed. Inherited descriptors for the same
|
|
1387
1393
|
socket inode are deduplicated, while unknown loopback ports, wildcard/host-address
|
|
1388
1394
|
listeners, process/socket/listener drift, or ambiguous ownership fail closed.
|
|
1389
|
-
Unit 18 uses the separately packaged fixed final command
|
|
1390
|
-
|
|
1395
|
+
Unit 18 uses the separately packaged fixed final command after activation.
|
|
1396
|
+
It accepts either the legacy-adoption topology with no canonical rollback or
|
|
1397
|
+
exactly one stopped, non-autostarted rollback; running/autostarted rollback,
|
|
1398
|
+
staging presence, and ambiguous canonical identities always fail closed:
|
|
1391
1399
|
UNIT18_TARGET_TMP=$(mktemp /run/sanctuary-unit18-target-audit.sh.XXXXXX)
|
|
1392
1400
|
/usr/bin/timeout -s KILL 20 /usr/bin/docker run --rm --pull=never --network none \
|
|
1393
1401
|
--entrypoint /bin/cat "$IMAGE_ID" /opt/ouro/deploy/unraid/sanctuary-unit18-target-audit.sh >"$UNIT18_TARGET_TMP"
|
|
@@ -1406,6 +1414,16 @@ Packaged Unit 16 acceptance execution:
|
|
|
1406
1414
|
--entrypoint /bin/cat "$IMAGE_ID" /opt/ouro/deploy/unraid/sanctuary-unit16-run.sh >"$UNIT16_LAUNCHER_TMP"
|
|
1407
1415
|
install -m 0755 -o root -g root "$UNIT16_LAUNCHER_TMP" "$UNIT16_ROOT/sanctuary-unit16-run.sh"
|
|
1408
1416
|
rm -f -- "$UNIT16_LAUNCHER_TMP"
|
|
1417
|
+
After final activation, create the disjoint final-profile roots and invoke the
|
|
1418
|
+
same immutable launcher with its only non-staging selector. The selector is a
|
|
1419
|
+
closed `staging|final` enum; it never accepts a container name, ID, or path:
|
|
1420
|
+
UNIT18_ROOT=/mnt/user/appdata/ouro-butler/acceptance/final
|
|
1421
|
+
install -d -m 0700 -o 10001 -g 10001 "$UNIT18_ROOT/configs" "$UNIT18_ROOT/evidence"
|
|
1422
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize evidence-snapshot
|
|
1423
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final evidence-snapshot evidence-snapshot.json
|
|
1424
|
+
Use `--profile final` on every final pre-reboot and post-reboot materialize/run
|
|
1425
|
+
command. Omitting it remains the byte-for-byte Unit 16 staging path. Never mix
|
|
1426
|
+
staging and final config/evidence roots or reuse a config across profiles.
|
|
1409
1427
|
Never hand-author a Unit 16 config. Materialize it from the exact image, then
|
|
1410
1428
|
execute it in the order below. Before every execution the launcher regenerates
|
|
1411
1429
|
the config from the packaged fixed contract and requires byte-for-byte equality.
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"adapterTimeoutMs": 240000,
|
|
6
6
|
"telegramTimeoutMs": 10000,
|
|
7
7
|
"deploymentTargetProfiles": {
|
|
8
|
-
"staging": { "command": "sanctuary-unit16-run.sh", "containerName": "ouro-butler-staging", "requiredRunning": 1, "restartPolicy": "unless-stopped", "networkMode": "host", "inboundTcpListeners": 0, "inboundUdpListeners": 0, "loopbackTcpControls": [6876] },
|
|
9
|
-
"final": { "command": "sanctuary-unit18-target-audit.sh", "containerName": "ouro-butler", "requiredRunning": 1, "
|
|
8
|
+
"staging": { "command": "sanctuary-unit16-run.sh", "acceptanceInvocation": "sanctuary-unit16-run.sh <image-id> ...", "containerName": "ouro-butler-staging", "requiredRunning": 1, "restartPolicy": "unless-stopped", "networkMode": "host", "inboundTcpListeners": 0, "inboundUdpListeners": 0, "loopbackTcpControls": [6876] },
|
|
9
|
+
"final": { "command": "sanctuary-unit18-target-audit.sh", "acceptanceInvocation": "sanctuary-unit16-run.sh <image-id> --profile final ...", "containerName": "ouro-butler", "requiredRunning": 1, "optionalStopped": "ouro-butler-rollback", "restartPolicy": "unless-stopped", "networkMode": "host", "inboundTcpListeners": 0, "inboundUdpListeners": 0, "loopbackTcpControls": [6876] }
|
|
10
10
|
},
|
|
11
11
|
"configMaterializer": {
|
|
12
12
|
"invocation": "sanctuary-acceptance-adapter.sh materialize-config <command> [before|after]",
|
|
@@ -11,8 +11,8 @@ const IMAGE_ID = /^sha256:[0-9a-f]{64}$/u
|
|
|
11
11
|
const CONTAINER_ID = /^[0-9a-f]{64}$/u
|
|
12
12
|
const CANONICAL_NAMES = ["ouro-butler", "ouro-butler-staging", "ouro-butler-rollback"]
|
|
13
13
|
const PROFILES = Object.freeze({
|
|
14
|
-
staging: Object.freeze({ name: "staging", containerName: "ouro-butler-staging", requiredStopped: [], forbidden: ["ouro-butler", "ouro-butler-rollback"] }),
|
|
15
|
-
final: Object.freeze({ name: "final", containerName: "ouro-butler", requiredStopped: ["ouro-butler-rollback"], forbidden: ["ouro-butler-staging"] }),
|
|
14
|
+
staging: Object.freeze({ name: "staging", containerName: "ouro-butler-staging", requiredStopped: [], optionalStopped: [], forbidden: ["ouro-butler", "ouro-butler-rollback"] }),
|
|
15
|
+
final: Object.freeze({ name: "final", containerName: "ouro-butler", requiredStopped: [], optionalStopped: ["ouro-butler-rollback"], forbidden: ["ouro-butler-staging"] }),
|
|
16
16
|
})
|
|
17
17
|
const DOCUMENTED_UNIX_CONTROLS = [
|
|
18
18
|
"/tmp/ouroboros-daemon.sock",
|
|
@@ -133,6 +133,10 @@ function attestDeploymentTarget(input) {
|
|
|
133
133
|
const stopped = byName.get(name)
|
|
134
134
|
if (!stopped || stopped.running || stopped.autoStart) throw new Error("required rollback target state is invalid")
|
|
135
135
|
}
|
|
136
|
+
for (const name of profile.optionalStopped) {
|
|
137
|
+
const stopped = byName.get(name)
|
|
138
|
+
if (stopped && (stopped.running || stopped.autoStart)) throw new Error("optional rollback target state is invalid")
|
|
139
|
+
}
|
|
136
140
|
for (const name of profile.forbidden) if (byName.has(name)) throw new Error("mixed deployment target state is invalid")
|
|
137
141
|
const activeRunningCardinality = inspected.filter(({ running }) => running).length
|
|
138
142
|
if (activeRunningCardinality !== 1) throw new Error("active Butler running cardinality must equal one")
|
|
@@ -168,7 +168,21 @@ function autostartFileExact() {
|
|
|
168
168
|
: counts.production === 1 && counts.staging === 0 && counts.rollback === 0 && counts.legacy === 0
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
function optionalStoppedContainerExact(name, containerId, run = spawnSync) {
|
|
172
|
+
if (name !== "ouro-butler-rollback" || !SHA256.test(containerId)) return false
|
|
173
|
+
const template = '{"containerId":{{json .Id}},"name":{{json .Name}},"running":{{json .State.Running}}}'
|
|
174
|
+
const result = run(DOCKER, ["inspect", "--format", template, containerId], {
|
|
175
|
+
cwd: "/", encoding: "utf8", timeout: 20_000, maxBuffer: 64 * 1024,
|
|
176
|
+
env: { PATH: "/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" }, stdio: ["ignore", "pipe", "ignore"],
|
|
177
|
+
})
|
|
178
|
+
if (result.error || result.status !== 0) return false
|
|
179
|
+
try {
|
|
180
|
+
const value = object(JSON.parse(result.stdout ?? ""), "optional rollback inspection")
|
|
181
|
+
return value.containerId === containerId && value.name === `/${name}` && value.running === false
|
|
182
|
+
} catch { return false }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function queryGraphqlAutostart(records = inventoryRecords(), fetchImpl = fetch, expectedContainerId = activeContainerId, profile = activeProfile, inspectOptionalStopped = optionalStoppedContainerExact) {
|
|
172
186
|
text(expectedContainerId, "attested target container id", SHA256)
|
|
173
187
|
const matches = records.filter((record) => record.name === "Butler RO" && record.roles.length === 0
|
|
174
188
|
&& JSON.stringify(flattened(record)) === JSON.stringify(RO_PERMISSIONS))
|
|
@@ -200,12 +214,16 @@ async function queryGraphqlAutostart(records = inventoryRecords(), fetchImpl = f
|
|
|
200
214
|
if (!identity || identity[1] !== serverIdentity[1] || typeof container.autoStart !== "boolean" || topology.has(canonical[0])) return false
|
|
201
215
|
topology.set(canonical[0], { containerId: identity[2], autoStart: container.autoStart })
|
|
202
216
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
217
|
+
const requiredNames = new Set([profile.containerName, ...profile.requiredStopped])
|
|
218
|
+
const allowedNames = new Set([...requiredNames, ...profile.optionalStopped])
|
|
219
|
+
if ([...requiredNames].some((name) => !topology.has(name)) || [...topology.keys()].some((name) => !allowedNames.has(name))
|
|
205
220
|
|| profile.forbidden.some((name) => topology.has(name))) return false
|
|
206
221
|
const target = topology.get(profile.containerName)
|
|
207
222
|
if (!target || target.containerId !== expectedContainerId || target.autoStart !== true) return false
|
|
208
|
-
return profile.requiredStopped.every((name) =>
|
|
223
|
+
return [...profile.requiredStopped, ...profile.optionalStopped].every((name) => {
|
|
224
|
+
const stopped = topology.get(name)
|
|
225
|
+
return !stopped || (stopped.autoStart === false && inspectOptionalStopped(name, stopped.containerId))
|
|
226
|
+
})
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
function updaterDisabled(expectedImage) {
|
|
@@ -1763,6 +1781,7 @@ export {
|
|
|
1763
1781
|
productionProcessBindingDigest,
|
|
1764
1782
|
observeRebootPreflight,
|
|
1765
1783
|
parseVaultStatus,
|
|
1784
|
+
optionalStoppedContainerExact,
|
|
1766
1785
|
queryGraphqlAutostart,
|
|
1767
1786
|
readBoundedProcStatus,
|
|
1768
1787
|
driveDuplicateCallbacks,
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
set -eu
|
|
3
3
|
|
|
4
|
-
CONFIG_ROOT=/mnt/user/appdata/ouro-butler/acceptance/configs
|
|
5
|
-
EVIDENCE_ROOT=/mnt/user/appdata/ouro-butler/acceptance/evidence
|
|
6
4
|
RUNTIME_ROOT=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli
|
|
7
5
|
BUNDLE_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
6
|
+
test "$#" -ge 1 || exit 2
|
|
7
|
+
IMAGE_ID=$1
|
|
8
|
+
shift
|
|
8
9
|
TARGET_PROFILE=staging
|
|
9
|
-
|
|
10
|
+
if test "${1:-}" = --profile; then
|
|
11
|
+
test "$#" -ge 2 || exit 2
|
|
12
|
+
TARGET_PROFILE=$2
|
|
13
|
+
shift 2
|
|
14
|
+
fi
|
|
15
|
+
case "$TARGET_PROFILE" in
|
|
16
|
+
staging)
|
|
17
|
+
CONFIG_ROOT=/mnt/user/appdata/ouro-butler/acceptance/configs
|
|
18
|
+
EVIDENCE_ROOT=/mnt/user/appdata/ouro-butler/acceptance/evidence
|
|
19
|
+
PRODUCTION_CONTAINER=ouro-butler-staging
|
|
20
|
+
;;
|
|
21
|
+
final)
|
|
22
|
+
CONFIG_ROOT=/mnt/user/appdata/ouro-butler/acceptance/final/configs
|
|
23
|
+
EVIDENCE_ROOT=/mnt/user/appdata/ouro-butler/acceptance/final/evidence
|
|
24
|
+
PRODUCTION_CONTAINER=ouro-butler
|
|
25
|
+
;;
|
|
26
|
+
*) exit 2 ;;
|
|
27
|
+
esac
|
|
10
28
|
TARGET_CONTAINER_ID=
|
|
11
|
-
|
|
12
|
-
MODE
|
|
29
|
+
MODE=${1:-}
|
|
30
|
+
case "$MODE" in --*) exit 2 ;; esac
|
|
13
31
|
BROKER_PID=
|
|
14
32
|
PRIVATE_ROOT=
|
|
15
33
|
PRODUCTION_STOPPED=no
|
|
@@ -57,26 +75,16 @@ cleanup_unit16() {
|
|
|
57
75
|
}
|
|
58
76
|
trap cleanup_unit16 EXIT HUP INT TERM
|
|
59
77
|
|
|
60
|
-
IMAGE_DIGEST=${IMAGE_ID#sha256:}
|
|
61
|
-
test "$IMAGE_DIGEST" != "$IMAGE_ID" || exit 2
|
|
62
|
-
test "${#IMAGE_DIGEST}" -eq 64 || exit 2
|
|
63
|
-
case "$IMAGE_DIGEST" in *[!0-9a-f]*) exit 2 ;; esac
|
|
64
|
-
test "$(/usr/bin/timeout -s KILL 20 /usr/bin/docker image inspect --format '{{.Id}}' "$IMAGE_ID")" = "$IMAGE_ID" || exit 1
|
|
65
|
-
test -d "$CONFIG_ROOT" && test ! -L "$CONFIG_ROOT" || exit 1
|
|
66
|
-
test -d "$EVIDENCE_ROOT" && test ! -L "$EVIDENCE_ROOT" || exit 1
|
|
67
|
-
test "$(stat -c '%u:%g %a' "$CONFIG_ROOT")" = "10001:10001 700" || exit 1
|
|
68
|
-
test "$(stat -c '%u:%g %a' "$EVIDENCE_ROOT")" = "10001:10001 700" || exit 1
|
|
69
|
-
|
|
70
78
|
case "$MODE" in
|
|
71
79
|
materialize)
|
|
72
|
-
test "$#" -eq
|
|
73
|
-
COMMAND=${
|
|
74
|
-
PHASE=${
|
|
80
|
+
test "$#" -eq 2 || { test "$#" -eq 3 && test "$2" = cursor-snapshot; } || exit 2
|
|
81
|
+
COMMAND=${2:-}
|
|
82
|
+
PHASE=${3:-}
|
|
75
83
|
;;
|
|
76
84
|
*)
|
|
77
|
-
test "$#" -eq
|
|
85
|
+
test "$#" -eq 2 || exit 2
|
|
78
86
|
COMMAND=$MODE
|
|
79
|
-
CONFIG_NAME=${
|
|
87
|
+
CONFIG_NAME=${2:-}
|
|
80
88
|
;;
|
|
81
89
|
esac
|
|
82
90
|
case "$COMMAND" in
|
|
@@ -84,6 +92,16 @@ case "$COMMAND" in
|
|
|
84
92
|
*) exit 2 ;;
|
|
85
93
|
esac
|
|
86
94
|
|
|
95
|
+
IMAGE_DIGEST=${IMAGE_ID#sha256:}
|
|
96
|
+
test "$IMAGE_DIGEST" != "$IMAGE_ID" || exit 2
|
|
97
|
+
test "${#IMAGE_DIGEST}" -eq 64 || exit 2
|
|
98
|
+
case "$IMAGE_DIGEST" in *[!0-9a-f]*) exit 2 ;; esac
|
|
99
|
+
test "$(/usr/bin/timeout -s KILL 20 /usr/bin/docker image inspect --format '{{.Id}}' "$IMAGE_ID")" = "$IMAGE_ID" || exit 1
|
|
100
|
+
test -d "$CONFIG_ROOT" && test ! -L "$CONFIG_ROOT" || exit 1
|
|
101
|
+
test -d "$EVIDENCE_ROOT" && test ! -L "$EVIDENCE_ROOT" || exit 1
|
|
102
|
+
test "$(stat -c '%u:%g %a' "$CONFIG_ROOT")" = "10001:10001 700" || exit 1
|
|
103
|
+
test "$(stat -c '%u:%g %a' "$EVIDENCE_ROOT")" = "10001:10001 700" || exit 1
|
|
104
|
+
|
|
87
105
|
PRIVATE_ROOT=$(mktemp -d /run/ouro-unit16.XXXXXX)
|
|
88
106
|
chmod 0700 "$PRIVATE_ROOT"
|
|
89
107
|
chown 0:0 "$PRIVATE_ROOT"
|
|
@@ -114,10 +132,10 @@ start_broker() {
|
|
|
114
132
|
TARGET_CONTAINER_ID=$(/usr/local/bin/node -e '
|
|
115
133
|
const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const deployment = value && value.deployment;
|
|
116
134
|
if (value?.schemaVersion !== "sanctuary-effective-deployment-v1" || deployment?.schemaVersion !== "sanctuary-deployment-target-v1"
|
|
117
|
-
|| deployment.profile !==
|
|
135
|
+
|| deployment.profile !== process.argv[3] || deployment.targetContainerName !== process.argv[4]
|
|
118
136
|
|| deployment.targetImageId !== process.argv[2] || !/^[0-9a-f]{64}$/.test(deployment.targetContainerId)) process.exit(1);
|
|
119
137
|
process.stdout.write(deployment.targetContainerId);
|
|
120
|
-
' "$PRIVATE_ROOT/deployment-target.json" "$IMAGE_ID")
|
|
138
|
+
' "$PRIVATE_ROOT/deployment-target.json" "$IMAGE_ID" "$TARGET_PROFILE" "$PRODUCTION_CONTAINER")
|
|
121
139
|
test -n "$TARGET_CONTAINER_ID" || return 1
|
|
122
140
|
/usr/local/bin/node "$BROKER_PROGRAM" "$TARGET_PROFILE" "$TARGET_CONTAINER_ID" "$BROKER_SOCKET" "$CLOSED_INVENTORY" "$IMAGE_ID" "$BROKER_SNAPSHOT" </dev/null >/dev/null 2>&1 &
|
|
123
141
|
BROKER_PID=$!
|
|
@@ -139,13 +139,20 @@ async function loadContainerCredentialBootstrap(enabledAgents, options = {}) {
|
|
|
139
139
|
}
|
|
140
140
|
if (loaded.has(message.agentName))
|
|
141
141
|
throw new Error("container credential bootstrap contains a duplicate agent");
|
|
142
|
-
if (!(0, runtime_credentials_1.isRuntimeCredentialBootstrapMessage)(message))
|
|
143
|
-
throw new Error("container credential bootstrap message is invalid");
|
|
144
142
|
loaded.add(message.agentName);
|
|
145
143
|
}
|
|
146
|
-
const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)().machineId;
|
|
147
144
|
const migration = options.machineIdMigration;
|
|
145
|
+
const discardProviderCredentialRecords = migration?.discardProviderCredentialRecords;
|
|
146
|
+
if (discardProviderCredentialRecords === undefined) {
|
|
147
|
+
for (const message of envelope.credentials) {
|
|
148
|
+
if (!(0, runtime_credentials_1.isRuntimeCredentialBootstrapMessage)(message)) {
|
|
149
|
+
throw new Error("container credential bootstrap message is invalid");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)().machineId;
|
|
148
154
|
let messages = envelope.credentials;
|
|
155
|
+
let discardedProviderRecordCount = 0;
|
|
149
156
|
if (migration) {
|
|
150
157
|
const { sourceMachineId, targetMachineId } = migration;
|
|
151
158
|
if (!sourceMachineId
|
|
@@ -163,7 +170,46 @@ async function loadContainerCredentialBootstrap(enabledAgents, options = {}) {
|
|
|
163
170
|
throw new Error("container credential bootstrap machineId migration source does not match");
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
|
-
|
|
173
|
+
if (discardProviderCredentialRecords !== undefined) {
|
|
174
|
+
if (!isRecord(discardProviderCredentialRecords) || !Array.isArray(discardProviderCredentialRecords.providers)) {
|
|
175
|
+
throw new Error("container credential bootstrap provider discard policy is invalid");
|
|
176
|
+
}
|
|
177
|
+
const configuredProviders = discardProviderCredentialRecords.providers;
|
|
178
|
+
if (configuredProviders.length === 0
|
|
179
|
+
|| configuredProviders.some((provider) => (typeof provider !== "string" || provider.length === 0 || provider !== provider.trim()))
|
|
180
|
+
|| new Set(configuredProviders).size !== configuredProviders.length) {
|
|
181
|
+
throw new Error("container credential bootstrap provider discard allowlist is invalid");
|
|
182
|
+
}
|
|
183
|
+
const legacyProviders = [];
|
|
184
|
+
for (const message of messages) {
|
|
185
|
+
const records = message.providerCredentialRecords;
|
|
186
|
+
if (records === undefined)
|
|
187
|
+
continue;
|
|
188
|
+
if (!Array.isArray(records) || records.length === 0) {
|
|
189
|
+
throw new Error("container credential bootstrap discarded provider records are invalid");
|
|
190
|
+
}
|
|
191
|
+
for (const record of records) {
|
|
192
|
+
if (!isRecord(record) || typeof record.provider !== "string" || record.provider.length === 0) {
|
|
193
|
+
throw new Error("container credential bootstrap discarded provider record is invalid");
|
|
194
|
+
}
|
|
195
|
+
legacyProviders.push(record.provider);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (new Set(legacyProviders).size !== legacyProviders.length) {
|
|
199
|
+
throw new Error("container credential bootstrap discarded providers contain duplicates");
|
|
200
|
+
}
|
|
201
|
+
if ([...legacyProviders].sort().join(",")
|
|
202
|
+
!== [...configuredProviders].sort().join(",")) {
|
|
203
|
+
throw new Error("container credential bootstrap discarded providers do not match the allowlist");
|
|
204
|
+
}
|
|
205
|
+
discardedProviderRecordCount = legacyProviders.length;
|
|
206
|
+
}
|
|
207
|
+
messages = messages.map((message) => {
|
|
208
|
+
const projected = { ...message, machineId: targetMachineId };
|
|
209
|
+
if (discardProviderCredentialRecords !== undefined)
|
|
210
|
+
delete projected.providerCredentialRecords;
|
|
211
|
+
return projected;
|
|
212
|
+
});
|
|
167
213
|
}
|
|
168
214
|
else {
|
|
169
215
|
for (const message of messages) {
|
|
@@ -172,6 +218,13 @@ async function loadContainerCredentialBootstrap(enabledAgents, options = {}) {
|
|
|
172
218
|
}
|
|
173
219
|
}
|
|
174
220
|
}
|
|
221
|
+
if (discardProviderCredentialRecords !== undefined) {
|
|
222
|
+
for (const message of messages) {
|
|
223
|
+
if (!(0, runtime_credentials_1.isRuntimeCredentialBootstrapMessage)(message)) {
|
|
224
|
+
throw new Error("container credential bootstrap message is invalid");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
175
228
|
const persist = options.persist ?? ((message) => (0, runtime_credentials_1.persistRuntimeCredentialBootstrapMessage)(message, {
|
|
176
229
|
machineId,
|
|
177
230
|
}));
|
|
@@ -198,6 +251,18 @@ async function loadContainerCredentialBootstrap(enabledAgents, options = {}) {
|
|
|
198
251
|
}
|
|
199
252
|
}
|
|
200
253
|
deleteDurably(consumingPath);
|
|
254
|
+
if (discardedProviderRecordCount > 0) {
|
|
255
|
+
(0, runtime_1.emitNervesEvent)({
|
|
256
|
+
component: "config/identity",
|
|
257
|
+
event: "config.container_provider_credentials_discarded",
|
|
258
|
+
message: "discarded allowlisted legacy provider credentials during container migration",
|
|
259
|
+
meta: {
|
|
260
|
+
agentCount: loaded.size,
|
|
261
|
+
recordCount: discardedProviderRecordCount,
|
|
262
|
+
mode: "machine-id-migration",
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
201
266
|
(0, runtime_1.emitNervesEvent)({
|
|
202
267
|
component: "config/identity",
|
|
203
268
|
event: "config.container_credentials_loaded",
|