@ouro.bot/cli 0.1.0-alpha.735 → 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 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.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
+ },
12
+ {
13
+ "version": "0.1.0-alpha.736",
14
+ "changes": [
15
+ "Fix the packaged Unraid image build to honor the repository peer dependency install contract.",
16
+ "Keep release lock metadata aligned and gate publication on building the packed Unraid image."
17
+ ]
18
+ },
4
19
  {
5
20
  "version": "0.1.0-alpha.735",
6
21
  "changes": [
@@ -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 --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"]
@@ -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, after rollback has
1390
- been retained stopped:
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, "requiredStopped": "ouro-butler-rollback", "restartPolicy": "unless-stopped", "networkMode": "host", "inboundTcpListeners": 0, "inboundUdpListeners": 0, "loopbackTcpControls": [6876] }
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
- async function queryGraphqlAutostart(records = inventoryRecords(), fetchImpl = fetch, expectedContainerId = activeContainerId, profile = activeProfile) {
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 expectedNames = new Set([profile.containerName, ...profile.requiredStopped])
204
- if (topology.size !== expectedNames.size || [...expectedNames].some((name) => !topology.has(name))
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) => topology.get(name)?.autoStart === false)
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
- PRODUCTION_CONTAINER=ouro-butler-staging
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
- IMAGE_ID=${1:-}
12
- MODE=${2:-}
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 3 || { test "$#" -eq 4 && test "$3" = cursor-snapshot; } || exit 2
73
- COMMAND=${3:-}
74
- PHASE=${4:-}
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 3 || exit 2
85
+ test "$#" -eq 2 || exit 2
78
86
  COMMAND=$MODE
79
- CONFIG_NAME=${3:-}
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 !== "staging" || deployment.targetContainerName !== "ouro-butler-staging"
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=$!