@ouro.bot/cli 0.1.0-alpha.736 → 0.1.0-alpha.737
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 +8 -0
- package/deploy/unraid/Dockerfile +17 -1
- package/deploy/unraid/README.txt +14 -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/npm-shrinkwrap.json +1634 -41
- package/package.json +3 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
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.737",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Ship the integrity-locked Bitwarden CLI in the non-root Unraid runtime so fresh Sanctuary vault access never depends on an impossible runtime install.",
|
|
8
|
+
"Add a closed final-production acceptance profile with isolated evidence roots while preserving the existing fixed staging path.",
|
|
9
|
+
"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."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
4
12
|
{
|
|
5
13
|
"version": "0.1.0-alpha.736",
|
|
6
14
|
"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
|
@@ -1386,8 +1386,10 @@ Packaged deployment-target containment gates:
|
|
|
1386
1386
|
types (including datagrams) fail closed. Inherited descriptors for the same
|
|
1387
1387
|
socket inode are deduplicated, while unknown loopback ports, wildcard/host-address
|
|
1388
1388
|
listeners, process/socket/listener drift, or ambiguous ownership fail closed.
|
|
1389
|
-
Unit 18 uses the separately packaged fixed final command
|
|
1390
|
-
|
|
1389
|
+
Unit 18 uses the separately packaged fixed final command after activation.
|
|
1390
|
+
It accepts either the legacy-adoption topology with no canonical rollback or
|
|
1391
|
+
exactly one stopped, non-autostarted rollback; running/autostarted rollback,
|
|
1392
|
+
staging presence, and ambiguous canonical identities always fail closed:
|
|
1391
1393
|
UNIT18_TARGET_TMP=$(mktemp /run/sanctuary-unit18-target-audit.sh.XXXXXX)
|
|
1392
1394
|
/usr/bin/timeout -s KILL 20 /usr/bin/docker run --rm --pull=never --network none \
|
|
1393
1395
|
--entrypoint /bin/cat "$IMAGE_ID" /opt/ouro/deploy/unraid/sanctuary-unit18-target-audit.sh >"$UNIT18_TARGET_TMP"
|
|
@@ -1406,6 +1408,16 @@ Packaged Unit 16 acceptance execution:
|
|
|
1406
1408
|
--entrypoint /bin/cat "$IMAGE_ID" /opt/ouro/deploy/unraid/sanctuary-unit16-run.sh >"$UNIT16_LAUNCHER_TMP"
|
|
1407
1409
|
install -m 0755 -o root -g root "$UNIT16_LAUNCHER_TMP" "$UNIT16_ROOT/sanctuary-unit16-run.sh"
|
|
1408
1410
|
rm -f -- "$UNIT16_LAUNCHER_TMP"
|
|
1411
|
+
After final activation, create the disjoint final-profile roots and invoke the
|
|
1412
|
+
same immutable launcher with its only non-staging selector. The selector is a
|
|
1413
|
+
closed `staging|final` enum; it never accepts a container name, ID, or path:
|
|
1414
|
+
UNIT18_ROOT=/mnt/user/appdata/ouro-butler/acceptance/final
|
|
1415
|
+
install -d -m 0700 -o 10001 -g 10001 "$UNIT18_ROOT/configs" "$UNIT18_ROOT/evidence"
|
|
1416
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final materialize evidence-snapshot
|
|
1417
|
+
"$UNIT16_ROOT/sanctuary-unit16-run.sh" "$IMAGE_ID" --profile final evidence-snapshot evidence-snapshot.json
|
|
1418
|
+
Use `--profile final` on every final pre-reboot and post-reboot materialize/run
|
|
1419
|
+
command. Omitting it remains the byte-for-byte Unit 16 staging path. Never mix
|
|
1420
|
+
staging and final config/evidence roots or reuse a config across profiles.
|
|
1409
1421
|
Never hand-author a Unit 16 config. Materialize it from the exact image, then
|
|
1410
1422
|
execute it in the order below. Before every execution the launcher regenerates
|
|
1411
1423
|
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=$!
|