@ouro.bot/cli 0.1.0-alpha.990 → 0.1.0-alpha.991

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.
Files changed (37) hide show
  1. package/changelog.json +6 -0
  2. package/deploy/unraid/Dockerfile +15 -2
  3. package/deploy/unraid/README.txt +44 -9
  4. package/deploy/unraid/github_known_hosts +3 -0
  5. package/deploy/unraid/ouro-sync-ssh +98 -0
  6. package/deploy/unraid/ouro-sync.sudoers +3 -0
  7. package/deploy/unraid/sanctuary-butler-upgrade.mjs +26 -2
  8. package/deploy/unraid/sanctuary-replay-gate.mjs +361 -8
  9. package/deploy/unraid/sanctuary-sync-cutover.sh +1039 -0
  10. package/deploy/unraid/sanctuary.ouro/agent.json +2 -1
  11. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  12. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +3 -1
  13. package/deploy/unraid/sanctuary.xml +2 -1
  14. package/dist/heart/config.js +2 -1
  15. package/dist/heart/daemon/agent-discovery.js +5 -1
  16. package/dist/heart/daemon/agent-home.js +67 -31
  17. package/dist/heart/daemon/boot-sync-probe.js +36 -10
  18. package/dist/heart/daemon/cli-exec.js +79 -20
  19. package/dist/heart/daemon/cli-help.js +12 -0
  20. package/dist/heart/daemon/cli-parse.js +14 -0
  21. package/dist/heart/daemon/container-spec-auditor-main.js +22 -6
  22. package/dist/heart/daemon/container-spec-auditor.js +18 -2
  23. package/dist/heart/daemon/daemon-entry.js +22 -0
  24. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1 -1
  25. package/dist/heart/daemon/sanctuary-bundle-migration.js +97 -1
  26. package/dist/heart/daemon/sanctuary-package-management.js +10 -0
  27. package/dist/heart/sync-lock.js +431 -0
  28. package/dist/heart/sync.js +1252 -399
  29. package/dist/mind/prompt.js +92 -19
  30. package/dist/repertoire/guardrails.js +285 -3
  31. package/dist/repertoire/tools-bundle.js +107 -185
  32. package/dist/repertoire/tools.js +5 -1
  33. package/dist/senses/pipeline.js +37 -3
  34. package/dist/senses/private-runtime.js +1 -6
  35. package/dist/senses/voice/twilio-phone.js +16 -16
  36. package/npm-shrinkwrap.json +2 -2
  37. package/package.json +1 -1
package/changelog.json CHANGED
@@ -1,6 +1,12 @@
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.991",
6
+ "changes": [
7
+ "The Butler's home now syncs to its own private GitHub repository: the bundle carries its own git, pushes through a deploy key it can never read, holds its first push for the owner's yes, and the host's hourly backup hands over through a cutover that can always roll back."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.990",
6
12
  "changes": [
@@ -7,14 +7,16 @@ ARG TARGETARCH
7
7
  ARG SUPERCRONIC_AMD64_SHA256=5adff01c5a797663948e656d2b61d10932369ee437eb5cb54fa872b2960f222b
8
8
  ARG SUPERCRONIC_ARM64_SHA256=c0576a8eb092e3f79108ed0a2155a25c7766af78456e5a6070e54757ef513bfe
9
9
  RUN apt-get update \
10
- && apt-get install -y --no-install-recommends ca-certificates curl procps python3 make g++ \
10
+ && apt-get install -y --no-install-recommends ca-certificates curl procps python3 make g++ git openssh-client sudo \
11
11
  && rm -rf /var/lib/apt/lists/* \
12
12
  && case "$TARGETARCH" in amd64) SUPERCRONIC_SHA256="$SUPERCRONIC_AMD64_SHA256" ;; arm64) SUPERCRONIC_SHA256="$SUPERCRONIC_ARM64_SHA256" ;; *) echo "unsupported architecture: $TARGETARCH" >&2; exit 1 ;; esac \
13
13
  && curl -fsSLo /usr/local/bin/supercronic "https://github.com/aptible/supercronic/releases/download/v0.2.46/supercronic-linux-${TARGETARCH}" \
14
14
  && echo "${SUPERCRONIC_SHA256} /usr/local/bin/supercronic" | sha256sum -c - \
15
15
  && chmod 0755 /usr/local/bin/supercronic \
16
16
  && groupadd --gid 10001 ouro \
17
- && useradd --uid 10001 --gid 10001 --home-dir /home/ouro --create-home --shell /usr/sbin/nologin ouro
17
+ && useradd --uid 10001 --gid 10001 --home-dir /home/ouro --create-home --shell /usr/sbin/nologin ouro \
18
+ && groupadd --system --gid 10002 ouro-sync \
19
+ && useradd --system --uid 10002 --gid 10002 --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin ouro-sync
18
20
 
19
21
  WORKDIR /opt/ouro
20
22
  COPY package.json npm-shrinkwrap.json ./
@@ -35,6 +37,17 @@ COPY skills ./skills
35
37
  COPY changelog.json ./changelog.json
36
38
  COPY deploy/unraid/container-runtime.json ./container-runtime.json
37
39
  COPY deploy/unraid /opt/ouro/deploy/unraid
40
+ # Bundle sync pushes over ssh with the deploy key mounted at /run/ouro-sync. The key is 10002:10002 mode 0400, so
41
+ # only ouro-sync can read it; the agent (ouro, 10001) reaches it only through sudo and the root-owned wrapper, which
42
+ # accepts nothing but git's ssh call for one GitHub repository and trusts only GitHub's published host keys.
43
+ COPY deploy/unraid/github_known_hosts /etc/ouro/github_known_hosts
44
+ COPY deploy/unraid/ouro-sync-ssh /usr/local/libexec/ouro-sync-ssh
45
+ COPY deploy/unraid/ouro-sync.sudoers /etc/sudoers.d/ouro-sync
46
+ RUN chown root:root /etc/ouro /etc/ouro/github_known_hosts /usr/local/libexec/ouro-sync-ssh /etc/sudoers.d/ouro-sync \
47
+ && chmod 0755 /etc/ouro /usr/local/libexec/ouro-sync-ssh \
48
+ && chmod 0644 /etc/ouro/github_known_hosts \
49
+ && chmod 0440 /etc/sudoers.d/ouro-sync \
50
+ && visudo -cf /etc/sudoers.d/ouro-sync
38
51
  COPY assets/sanctuary-host-launcher.sh /opt/ouro/deploy/unraid/sanctuary-host-launcher.sh
39
52
  RUN chown -R 10001:10001 /opt/ouro /home/ouro
40
53
 
@@ -45,7 +45,34 @@ Effective-spec audit helper:
45
45
  Before any sequence, define this helper. It captures one actual container
46
46
  inspect and its reviewed image inspect without printing either, invokes the
47
47
  packaged auditor from the explicit validated runner image argument, and
48
- removes its mode-0600 inputs on success or shell exit:
48
+ removes its mode-0600 inputs on success or shell exit. bundle_sync_enabled
49
+ prints true or false from the bundle's agent.json and fails, with a fixed
50
+ message, when it is a symlink, is not a regular file (a FIFO would block the
51
+ read), or cannot be read or parsed; audits of the container being installed pass `bundle` so the
52
+ auditor requires the sync key mount while sync is on:
53
+ bundle_sync_enabled() {
54
+ BUNDLE_AGENT_JSON=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/agent.json
55
+ if test -L "$BUNDLE_AGENT_JSON"; then
56
+ printf '%s\n' "bundle sync check: agent.json is a symlink; refusing to read it" >&2
57
+ return 1
58
+ fi
59
+ /usr/local/bin/node -e '
60
+ const fs = require("node:fs");
61
+ let agent;
62
+ try {
63
+ const fd = fs.openSync(process.argv[1], fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
64
+ if (!fs.fstatSync(fd).isFile()) {
65
+ process.stderr.write("bundle sync check: agent.json is not a regular file; refusing to read it\n");
66
+ process.exit(1);
67
+ }
68
+ agent = JSON.parse(fs.readFileSync(fd, "utf8"));
69
+ } catch {
70
+ process.stderr.write("bundle sync check: agent.json is unreadable or not valid JSON\n");
71
+ process.exit(1);
72
+ }
73
+ process.stdout.write(agent?.sync?.enabled === true ? "true" : "false");
74
+ ' "$BUNDLE_AGENT_JSON"
75
+ }
49
76
  audit_effective() {
50
77
  (
51
78
  AUDIT_CONTAINER=$1
@@ -54,6 +81,7 @@ Effective-spec audit helper:
54
81
  AUDIT_MOUNT_CONTRACT=${4-}
55
82
  AUDIT_EXPECTED_IMAGE_REFERENCE=${5-}
56
83
  AUDIT_EXPECTED_ICON=${6-}
84
+ AUDIT_SYNC_SOURCE=${7-}
57
85
  validate_exact_image_id "$AUDIT_EXPECTED_IMAGE" || return $?
58
86
  validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
59
87
  test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
@@ -62,6 +90,11 @@ Effective-spec audit helper:
62
90
  test -n "$AUDIT_EXPECTED_IMAGE_REFERENCE" || return 1
63
91
  test "$AUDIT_EXPECTED_ICON" = https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png || return 1
64
92
  set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" --expected-image-reference "$AUDIT_EXPECTED_IMAGE_REFERENCE" --expected-icon "$AUDIT_EXPECTED_ICON"
93
+ case "$AUDIT_SYNC_SOURCE" in
94
+ bundle) AUDIT_SYNC_ENABLED=$(bundle_sync_enabled) || return $?; set -- "$@" --sync-enabled "$AUDIT_SYNC_ENABLED" ;;
95
+ "") ;;
96
+ *) return 1 ;;
97
+ esac
65
98
  ;;
66
99
  legacy-alpha742) set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" ;;
67
100
  *) return 1 ;;
@@ -920,7 +953,8 @@ Effective-spec audit helper:
920
953
  validate_exact_image_id "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" || return $?
921
954
  printf '%s\n' "$REGISTERED_TEMPLATE_VERSION_IMAGE" | grep -Eq '^ghcr\.io/ourostack/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || return $?
922
955
  test -f "$REGISTERED_TEMPLATE_PATH" && test ! -L "$REGISTERED_TEMPLATE_PATH" || return 1
923
- docker run --rm --pull=never --network=none --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh --mount "type=bind,src=$REGISTERED_TEMPLATE_PATH,dst=/audit/sanctuary.xml,readonly" "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /opt/ouro/deploy/unraid/container-runtime.json --expected-image-reference "$REGISTERED_TEMPLATE_VERSION_IMAGE" --mount-contract "$REGISTERED_TEMPLATE_MOUNT_CONTRACT" || return $?
956
+ REGISTERED_TEMPLATE_SYNC_ENABLED=$(bundle_sync_enabled) || return $?
957
+ docker run --rm --pull=never --network=none --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh --mount "type=bind,src=$REGISTERED_TEMPLATE_PATH,dst=/audit/sanctuary.xml,readonly" "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /opt/ouro/deploy/unraid/container-runtime.json --expected-image-reference "$REGISTERED_TEMPLATE_VERSION_IMAGE" --mount-contract "$REGISTERED_TEMPLATE_MOUNT_CONTRACT" --sync-enabled "$REGISTERED_TEMPLATE_SYNC_ENABLED" || return $?
924
958
  }
925
959
  verify_known_good_rollback_artifact() {
926
960
  EXPECTED_KNOWN_GOOD_IMAGE_ID=$1
@@ -1203,7 +1237,7 @@ Effective-spec audit helper:
1203
1237
  test "$(docker inspect --format '{{.Image}}' ouro-butler-rollback)" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
1204
1238
  test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
1205
1239
  assert_only_running_butler ouro-butler || return $?
1206
- audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1240
+ audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle || return $?
1207
1241
  migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
1208
1242
  chown 0:0 "$TEMPLATE_RECOVERY_INSPECTION" && chmod 0600 "$TEMPLATE_RECOVERY_INSPECTION" || return $?
1209
1243
  write_dockerman_recovery_evidence committing target-exact-committing "$TEMPLATE_RECOVERY_INSPECTION" "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
@@ -1255,7 +1289,7 @@ Effective-spec audit helper:
1255
1289
  TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
1256
1290
  test "$TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
1257
1291
  test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return 1
1258
- audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1292
+ audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle || return $?
1259
1293
  start_only_butler_for_recovery || return $?
1260
1294
  wait_butler_ready ouro-butler || return $?
1261
1295
  migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
@@ -1265,7 +1299,7 @@ Effective-spec audit helper:
1265
1299
  enable_butler_autostart || return $?
1266
1300
  /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null || return $?
1267
1301
  elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_STATE" = committing && test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$IMAGE_ID"; then
1268
- audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
1302
+ audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle || return $?
1269
1303
  wait_butler_ready ouro-butler || return $?
1270
1304
  verify_butler_autostart "1 0 0 0" || return $?
1271
1305
  migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
@@ -2109,12 +2143,13 @@ Update:
2109
2143
  INSTALLED_SPOOL_SIZE=$(findmnt -bn -o SIZE --target /boot/config/custom/ouro-events/spool)
2110
2144
  test "$INSTALLED_SPOOL_SIZE" -le 4194304
2111
2145
  }
2146
+ STAGED_SYNC_ENABLED=$(bundle_sync_enabled)
2112
2147
  docker run --rm --pull=never --network=none \
2113
2148
  --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
2114
2149
  --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
2115
2150
  --mount "type=bind,src=$STAGED_TEMPLATE,dst=/audit/sanctuary.xml,readonly" \
2116
2151
  --mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
2117
- "$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE" --mount-contract "$TARGET_MOUNT_CONTRACT"
2152
+ "$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE" --mount-contract "$TARGET_MOUNT_CONTRACT" --sync-enabled "$STAGED_SYNC_ENABLED"
2118
2153
  /usr/local/bin/node -e '
2119
2154
  const fs = require("node:fs");
2120
2155
  const [sourcePath, destinationPath, imageId] = process.argv.slice(1);
@@ -2134,7 +2169,7 @@ Update:
2134
2169
  --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
2135
2170
  --mount "type=bind,src=$STAGED_EXACT_TEMPLATE,dst=/audit/sanctuary.exact-image.xml,readonly" \
2136
2171
  --mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
2137
- "$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID" --mount-contract "$TARGET_MOUNT_CONTRACT"
2172
+ "$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID" --mount-contract "$TARGET_MOUNT_CONTRACT" --sync-enabled "$STAGED_SYNC_ENABLED"
2138
2173
  recover_dockerman_template_transaction
2139
2174
  recover_pending_sanctuary_bundle_migration "$IMAGE_ID"
2140
2175
  classify_sanctuary_update_source
@@ -2301,7 +2336,7 @@ ouro-butler-rollback
2301
2336
  if test "$(inspect_registry_manifest_digest "$VERSION_IMAGE")" = "$MANIFEST_DIGEST" \
2302
2337
  && test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID" \
2303
2338
  && create_sanctuary_container "$IMAGE_ID" "$VERSION_IMAGE" "$TARGET_MOUNT_CONTRACT" \
2304
- && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" \
2339
+ && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle \
2305
2340
  && assert_only_running_butler - \
2306
2341
  && /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-activate >/dev/null \
2307
2342
  && assert_only_running_butler ouro-butler \
@@ -2646,7 +2681,7 @@ Restore:
2646
2681
  && rm -f "$HOST_RESTORE_INSTALLER" \
2647
2682
  && docker image inspect "$IMAGE_ID" >/dev/null \
2648
2683
  && create_sanctuary_container "$IMAGE_ID" "$RESTORE_VERSION_IMAGE" "$RESTORE_MOUNT_CONTRACT" \
2649
- && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_MOUNT_CONTRACT" "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png \
2684
+ && audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_MOUNT_CONTRACT" "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png bundle \
2650
2685
  && assert_only_running_butler - \
2651
2686
  && docker start ouro-butler \
2652
2687
  && assert_only_running_butler ouro-butler \
@@ -0,0 +1,3 @@
1
+ github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
2
+ github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
3
+ github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
@@ -0,0 +1,98 @@
1
+ #!/bin/sh
2
+ # Bundle sync's ssh for git. Runs as ouro-sync under sudo, so only ouro-sync can read the deploy key.
3
+ # It accepts only git's own ssh invocation for one GitHub repository and passes nothing else to ssh.
4
+ # It runs no tool through PATH: validation is shell builtins only, and ssh is called by absolute path.
5
+ set -u
6
+ PATH=/usr/sbin:/usr/bin:/sbin:/bin
7
+ export PATH
8
+ # The character checks below use ranges, which follow the locale under bash.
9
+ LC_ALL=C
10
+ export LC_ALL
11
+
12
+ refuse() {
13
+ printf 'ouro-sync-ssh: refused: %s\n' "$1" >&2
14
+ exit 2
15
+ }
16
+
17
+ login=""
18
+ host=""
19
+ command=""
20
+ have_host=0
21
+ have_command=0
22
+ while [ "$#" -gt 0 ]; do
23
+ case "$1" in
24
+ -o)
25
+ [ "$#" -ge 2 ] && [ "$2" = "SendEnv=GIT_PROTOCOL" ] || refuse "only -o SendEnv=GIT_PROTOCOL is allowed"
26
+ shift 2
27
+ ;;
28
+ -p)
29
+ [ "$#" -ge 2 ] && [ "$2" = "22" ] || refuse "only -p 22 is allowed"
30
+ shift 2
31
+ ;;
32
+ -l)
33
+ [ "$#" -ge 2 ] && [ "$2" = "git" ] || refuse "only -l git is allowed"
34
+ login=git
35
+ shift 2
36
+ ;;
37
+ -*)
38
+ refuse "option $1 is not allowed"
39
+ ;;
40
+ *)
41
+ if [ "$have_host" -eq 0 ]; then
42
+ host=$1
43
+ have_host=1
44
+ elif [ "$have_command" -eq 0 ]; then
45
+ command=$1
46
+ have_command=1
47
+ else
48
+ refuse "too many arguments"
49
+ fi
50
+ shift
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ case "$host" in
56
+ git@github.com) [ -z "$login" ] || refuse "host must be git@github.com" ;;
57
+ github.com) [ "$login" = git ] || refuse "host must be git@github.com" ;;
58
+ *) refuse "host must be git@github.com" ;;
59
+ esac
60
+ [ "$have_command" -eq 1 ] || refuse "a git command is required"
61
+ case "$command" in
62
+ *"
63
+ "*) refuse "the git command must be one line" ;;
64
+ esac
65
+ bad_command="only git-upload-pack or git-receive-pack for one repository is allowed"
66
+ case "$command" in
67
+ "git-upload-pack "*) quoted=${command#git-upload-pack } ;;
68
+ "git-receive-pack "*) quoted=${command#git-receive-pack } ;;
69
+ *) refuse "$bad_command" ;;
70
+ esac
71
+ case "$quoted" in
72
+ *[!A-Za-z0-9._/\'-]*) refuse "$bad_command" ;;
73
+ esac
74
+ # Either 'owner/repo' with both quotes, or owner/repo with none.
75
+ case "$quoted" in
76
+ \'*\') repo=${quoted#\'}; repo=${repo%\'} ;;
77
+ *\'*) refuse "the repository's quotes must be balanced" ;;
78
+ *) repo=$quoted ;;
79
+ esac
80
+ case "$repo" in
81
+ */*/*) refuse "$bad_command" ;;
82
+ */*) ;;
83
+ *) refuse "$bad_command" ;;
84
+ esac
85
+ owner=${repo%%/*}
86
+ name=${repo#*/}
87
+ name=${name%.git}
88
+ for segment in "$owner" "$name"; do
89
+ case "$segment" in
90
+ "" | *[!A-Za-z0-9._-]*) refuse "$bad_command" ;;
91
+ . | ..) refuse "the repository path may not contain . or .." ;;
92
+ esac
93
+ done
94
+
95
+ exec /usr/bin/ssh -i /run/ouro-sync/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes \
96
+ -o UserKnownHostsFile=/etc/ouro/github_known_hosts -o BatchMode=yes \
97
+ -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 \
98
+ -F /dev/null git@github.com "$command"
@@ -0,0 +1,3 @@
1
+ Defaults!/usr/local/libexec/ouro-sync-ssh env_reset
2
+ Defaults!/usr/local/libexec/ouro-sync-ssh secure_path="/usr/sbin:/usr/bin:/sbin:/bin"
3
+ ouro ALL=(ouro-sync) NOPASSWD: /usr/local/libexec/ouro-sync-ssh
@@ -477,6 +477,11 @@ function enableResidentAutostart() {
477
477
  // read-only at /etc/ouro/trust/sanctuary. The bundle's recursive chown cannot reach it, and the resident cannot write it.
478
478
  export const TRUST_HOST_DIR = "/mnt/user/appdata/ouro-butler/trust/sanctuary"
479
479
  export const TRUST_MOUNT = `${TRUST_HOST_DIR}:/etc/ouro/trust/sanctuary:ro`
480
+ // The bundle sync deploy key: root places it on the host, and the Butler reads it through a read-only mount. The key can
481
+ // push to the owner's repo, so nothing inside the Butler can write the mount.
482
+ export const SYNC_HOST_DIR = "/mnt/user/appdata/ouro-butler/sync"
483
+ export const SYNC_TARGET = "/run/ouro-sync"
484
+ export const SYNC_MOUNT = `${SYNC_HOST_DIR}:${SYNC_TARGET}:ro`
480
485
 
481
486
  /** What is wrong with a host trust directory's stat, or null when it is a real root-owned directory closed to group and other writes. */
482
487
  export function trustDirIssue(stat) {
@@ -544,6 +549,7 @@ function recreateResident(version) {
544
549
  "-v", "/boot/config/custom/ouro-events/spool:/run/ouro-events:ro",
545
550
  "-v", "/run/ouro-authority:/run/ouro-authority:ro",
546
551
  "-v", TRUST_MOUNT,
552
+ "-v", SYNC_MOUNT,
547
553
  image(version)])
548
554
  const ref = docker(["inspect", CONTAINER, "--format", "{{.Config.Image}}"]).trim()
549
555
  if (ref !== image(version)) fail(`recreated resident is ${ref}, expected ${image(version)}`)
@@ -1195,6 +1201,23 @@ export function withoutPsycheConfig(text, parse) {
1195
1201
  return after && psycheConfigs(after) === 0 && after.children.length === before.children.length - 1 ? next : text
1196
1202
  }
1197
1203
 
1204
+ export const SYNC_CONFIG = ` <Config Name="Bundle sync key (read-only)" Target="${SYNC_TARGET}" Default="${SYNC_HOST_DIR}" Mode="ro" Description="Deploy key the Butler's bundle sync pushes with, owned by 10002:10002 mode 0400 so only the sync wrapper reads it. Read-only inside the Butler" Type="Path" Display="always" Required="true" Mask="false">${SYNC_HOST_DIR}</Config>`
1205
+
1206
+ /**
1207
+ * The DockerMan template with the sync key's read-only Config added before </Container>, so a DockerMan Apply keeps the
1208
+ * mount. `parse` is docker-man-template-xml.cjs's parseDockerManTemplateXml: a template it cannot read, one that already
1209
+ * has a Config for the target, or one where the insertion does not add exactly that element comes back unchanged.
1210
+ */
1211
+ export function withSyncConfig(text, parse) {
1212
+ const syncConfigs = (document) => document?.children.filter((child) => child.name === "Config" && child.attributes.Target === SYNC_TARGET).length
1213
+ const before = parse(text)
1214
+ if (!before || syncConfigs(before) !== 0) return text
1215
+ const close = text.lastIndexOf("</Container>")
1216
+ const next = `${text.slice(0, close)}${SYNC_CONFIG}\n${text.slice(close)}`
1217
+ const after = parse(next)
1218
+ return after && syncConfigs(after) === 1 && after.children.length === before.children.length + 1 ? next : text
1219
+ }
1220
+
1198
1221
  // Keep Unraid's DockerMan template (what the UI's Apply/Update uses) on the live image, without the old psyche mount.
1199
1222
  // Runs after the commit, so the parser comes from the installed package (this script is staged on its own, without siblings).
1200
1223
  function pinTemplate(version) {
@@ -1204,9 +1227,10 @@ function pinTemplate(version) {
1204
1227
  if (!next.includes(`<Repository>${image(version)}</Repository>`)) { console.log(" WARN: DockerMan template Repository not recognised; left unchanged"); return }
1205
1228
  try {
1206
1229
  const { parseDockerManTemplateXml } = createRequire(import.meta.url)(`${ROOT}/package/deploy/unraid/docker-man-template-xml.cjs`)
1207
- next = withoutPsycheConfig(next, parseDockerManTemplateXml)
1208
- } catch (error) { console.log(` WARN: could not load the DockerMan template parser (${error.message}); psyche Config left in place`) }
1230
+ next = withSyncConfig(withoutPsycheConfig(next, parseDockerManTemplateXml), parseDockerManTemplateXml)
1231
+ } catch (error) { console.log(` WARN: could not load the DockerMan template parser (${error.message}); psyche Config left in place, sync key Config not added`) }
1209
1232
  if (next.includes(`Target="${PSYCHE_TARGET}"`)) console.log(" WARN: DockerMan template still has a psyche Config; remove it before the next DockerMan Apply")
1233
+ if (!next.includes(`Target="${SYNC_TARGET}"`)) console.log(` WARN: DockerMan template has no ${SYNC_TARGET} Config; add it before the next DockerMan Apply`)
1210
1234
  if (next !== text) { writeFileSync(`${TEMPLATE}.prev`, text); writeFileSync(TEMPLATE, next) }
1211
1235
  ok(`DockerMan template pins ${image(version)}`)
1212
1236
  }