fix(wrapper): enforce capture policy in hook installs

Fixes #1002
This commit is contained in:
AkitaOnRails
2026-10-01 13:45:52 -03:00
parent 15a26421a3
commit 2ee5642815
10 changed files with 119 additions and 45 deletions
+8
View File
@@ -12,6 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
provider. (#1026)
### Fixed
- Fixed the recommended Linux/macOS Docker-wrapper `install-hooks` path
silently installing shell hooks that cannot enforce client-side capture
controls such as `[capture] ignore_paths` and allowlist mode. The wrapper now
uses its existing checksum-verified native host client for hook installation
and refresh, while an explicit `AI_MEMORY_HOOK_PLATFORM=posix|windows` keeps
the documented compatibility fallback and warning. Wrapper upgrades refresh
that stable native client before rewriting existing hook registrations.
(#1002)
- Fixed managed Codex sessions losing their startup continuity when a shared
Codex app-server daemon reports a finished run's stale `AI_MEMORY_RUN_ID`.
SessionStart now recovers only the sole live, undelivered Codex run in the
+8
View File
@@ -318,6 +318,14 @@ ai-memory install-mcp --client claude-code --apply
ai-memory install-hooks --agent claude-code --apply
```
On Linux and macOS, the Docker wrapper runs `install-hooks` through its
checksum-verified native host client. The installed hooks therefore enforce
client-side capture controls such as `[capture] ignore_paths` and allowlist
mode before an event reaches the spool or network. Set
`AI_MEMORY_HOOK_PLATFORM=posix` explicitly only when you need the legacy shell
compatibility path; the installer warns that path cannot enforce capture
policy v1.
The examples use `docker`; replace it with `podman` on a Podman host. The
wrapper automatically uses Podman when Docker is not installed. Set
`AI_MEMORY_DOCKER=podman` to force Podman when both engines are available.
+29 -8
View File
@@ -31,7 +31,7 @@
# likewise
# AI_MEMORY_NO_TTY=1 force non-interactive even on a real tty
# AI_MEMORY_NO_VERSION_CHECK=1 skip the once-per-day update check
# AI_MEMORY_NATIVE_BIN native binary used for managed host commands (optional)
# AI_MEMORY_NATIVE_BIN native binary used for host commands and hooks (optional)
# AI_MEMORY_WRAPPER_URL wrapper release asset override (optional; its
# .sha256 companion is required)
# AI_MEMORY_WRAPPER_SHA256_URL wrapper checksum URL override (optional)
@@ -359,6 +359,11 @@ cmd_upgrade() {
echo "→ pulling ${IMAGE}"
"${DOCKER}" pull "${IMAGE}"
rm -f "${NATIVE_RUNNER_DIR}/last-check"
echo "→ refreshing checksum-verified native host client"
native_host_binary >/dev/null || {
echo "ai-memory: upgrade stopped before hook refresh because the native host client could not be updated" >&2
return 1
}
local found_agents=()
if [ -d "${HOOKS_STAGE_DIR}" ]; then
@@ -466,6 +471,10 @@ native_host_binary() {
echo "ai-memory: AI_MEMORY_NATIVE_BIN is not executable: ${AI_MEMORY_NATIVE_BIN}" >&2
return 1
}
if [ "${AI_MEMORY_NATIVE_BIN}" -ef "$0" ]; then
echo "ai-memory: AI_MEMORY_NATIVE_BIN points to the container wrapper itself; use the native release binary" >&2
return 1
fi
printf '%s\n' "${AI_MEMORY_NATIVE_BIN}"
return 0
fi
@@ -475,8 +484,8 @@ native_host_binary() {
Linux) os="linux" ;;
Darwin) os="macos" ;;
*)
echo "ai-memory: the container wrapper cannot provide managed host launches on this OS" >&2
echo "install the native release binary to use ai-memory run/show/continue/resume/workstreams/rename-workstream" >&2
echo "ai-memory: the container wrapper cannot provide native host commands on this OS" >&2
echo "install the native release binary to use managed launches or native lifecycle hooks" >&2
return 1
;;
esac
@@ -564,6 +573,19 @@ case "${1:-}" in
NATIVE_HOST_BIN=$(native_host_binary)
exec "${NATIVE_HOST_BIN}" "${NATIVE_HOST_COMMAND}" "$@"
;;
install-hooks)
# Capture exclusions and allowlist mode are client-side privacy controls.
# The wrapper's stable, checksum-verified host client can enforce them;
# the helper-container script bundle cannot. Keep explicit compatibility
# selections available, but never choose the weaker path by default.
case "${AI_MEMORY_HOOK_PLATFORM:-}" in
posix | windows) ;;
*)
NATIVE_HOST_BIN=$(native_host_binary)
exec "${NATIVE_HOST_BIN}" "$@"
;;
esac
;;
esac
# ---- normal pass-through to the binary inside a container ----------------
@@ -578,11 +600,10 @@ if [ -z "${AI_MEMORY_NO_TTY:-}" ] && [ -t 0 ] && [ -t 1 ]; then
TTY_ARGS+=(-t)
fi
# The CLI runs INSIDE the container but renders hook config for the HOST, which
# has no local ai-memory binary. Native installs default to `posix-native` (the
# binary hook command); the wrapper must NOT inherit that or it would bake the
# *container's* binary path into the host's hook config. Force the shell-script
# platform (`posix`) unless the operator chose one explicitly.
# Commands that still reach the helper container must never bake its binary
# path into host config. Ordinary `install-hooks` was intercepted above and
# uses the stable native host client; an explicit script compatibility choice
# reaches here and remains script-based.
export AI_MEMORY_HOOK_PLATFORM="${AI_MEMORY_HOOK_PLATFORM:-posix}"
ENV_ARGS=()
+29 -22
View File
@@ -479,10 +479,13 @@ a script fallback.
`[capture] ignore_paths` is enforced only by native `ai-memory hook` commands
and generated OpenCode/OMP/Pi/OpenClaw integrations. Local installers select
native commands where supported; legacy `.sh`/`.ps1` hooks and remote-only or
Docker script bundles do not enforce it. Re-run `install-hooks --agent <agent>
--apply` or refresh/reinstall generated plugins after upgrading; installer
capability output reflects the selected integration. See the canonical
native commands where supported. The Linux/macOS Docker wrapper also routes
ordinary `install-hooks` through its checksum-verified native host client, so
the quick-start path is covered. Legacy `.sh`/`.ps1` hooks, an explicit
`AI_MEMORY_HOOK_PLATFORM=posix|windows`, `setup-agent`, and remote-only/manual
Docker script bundles do not enforce it. Re-run `install-hooks --agent
<agent> --apply` or refresh/reinstall generated plugins after upgrading;
installer capability output reflects the selected integration. See the canonical
[capture exclusions reference](marker-file.md#capture-exclusions).
Lifecycle observation bodies are bounded separately from the 10 MiB HTTP
@@ -538,9 +541,10 @@ Windows, so the usual install is covered. It is the *script* installs that are
not: the bundled shell/PowerShell hooks POST to the server directly and never
execute the binary, so nothing reads the mode. In practice that means the
legacy `posix`/`windows` platform override (`AI_MEMORY_HOOK_PLATFORM`), the
Docker host wrapper, and `setup-agent` snippets, which emit script commands by
design. `install-hooks --apply` prints the mode and warns when the install it
is writing cannot enforce it.
remote `setup-agent` flow, and manual Docker script-bundle installs. The
Linux/macOS Docker wrapper's ordinary `install-hooks` path uses its native host
client and is covered. `install-hooks --apply` prints the mode and warns when
the install it is writing cannot enforce it.
Within that boundary the mode is not per-agent: it is stored once in the data
directory and every native hook command reads it, whichever agent invoked it.
@@ -607,17 +611,14 @@ it flows (consolidation/reviewer prompts, and out to a cloud LLM provider if one
is configured) before enabling it.
Upgrading the binary is sufficient for native Claude Code installs, and pending
spooled events drain with the raw field stripped as well. Installs that run the
`.sh`/`.ps1` script fallback (the Docker script bundle or an explicit
`AI_MEMORY_HOOK_PLATFORM=posix`) cannot sanitize the assistant text, so a `Stop`
payload still carrying the raw field is dropped whole by the script rather than
POSTed verbatim. The Docker wrapper deliberately keeps script commands because a
binary path inside its helper container is not valid on the host; running
`install-hooks` through that wrapper refreshes the scripts but does not convert
them. To capture assistant text safely, install a native ai-memory client on the
agent host, then use that native executable to run
`install-hooks --agent claude-code --apply`. Even if the script fallback is
retained, the server still strips any raw field on receipt before persistence.
spooled events drain with the raw field stripped as well. The Linux/macOS Docker
wrapper installs the same native hook command through its checksum-verified
host client. Installs that explicitly select the `.sh`/`.ps1` compatibility
fallback (`AI_MEMORY_HOOK_PLATFORM=posix|windows`), or use a remote/manual
Docker script bundle, cannot sanitize the assistant text, so a `Stop` payload
still carrying the raw field is dropped whole by the script rather than POSTed
verbatim. Even when that fallback is retained, the server strips any raw field
on receipt before persistence.
Native `ai-memory hook --event ...` commands spool events locally. The POSIX
shell bundle spools too, but only on failure: it POSTs first and writes the
@@ -799,7 +800,9 @@ including Pi and Zero, have lifecycle capture paths through `install-hooks`.
> real `ai-memory.exe`, `args` = argv tokens for `hook --event ...`); other
> agents use native single command strings according to their hook schema.
> PowerShell/Git Bash script bundles are compatibility fallbacks and do not
> enforce capture-policy v1. Remote-only/Docker script installs still use the
> enforce capture-policy v1. The Linux/macOS Docker wrapper downloads a
> checksum-verified native host client for ordinary `install-hooks` calls.
> Remote-only/manual Docker script installs still use the
> two-step path: (1) `docker cp` bundled scripts to your home dir, (2)
> `docker run --rm install-hooks` renders the config snippet.
> OpenClaw, OpenCode, OMP, and Pi are different: they use generated
@@ -2508,7 +2511,11 @@ ai-memory install-hooks --agent claude-code --apply
```
The installed Docker wrapper runs CLI commands inside a short-lived
helper container. For local loopback servers, it automatically bridges
helper container for server/store operations, but runs `install-hooks` through
the stable checksum-verified native client under
`~/.local/share/ai-memory/native-runner/`. This keeps hook command paths valid
on the host and enforces capture exclusions before spooling. For local loopback
servers, the wrapper automatically bridges
that helper back to the host's `127.0.0.1:49374`, so `ai-memory status`,
`ai-memory search`, and `ai-memory bootstrap` work with the same default
URL as the generated agent config.
@@ -2585,8 +2592,8 @@ ai-memory upgrade
The command downloads the wrapper and its SHA-256 checksum from the latest
GitHub Release, refuses an unverified update, pulls the latest Docker
image, re-stages hook scripts under
`~/.local/share/ai-memory/hooks/<agent>/` for configured agents, and
image, refreshes the checksum-verified native host client, rewrites previously
staged hook registrations to that native command when supported, and
prints how to restart the server container so the new binary is used.
Re-running `install-hooks --apply` remains idempotent: ai-memory
replaces only the hook entries it owns and leaves unrelated hooks alone.
+5 -3
View File
@@ -37,9 +37,11 @@ normal Terminal.
which works from the host agent.
- Hooks are rendered for one of two platforms:
- `posix-native` — a direct `ai-memory hook --event …` call. The default for
native macOS/Linux Claude Code installs (cargo / release binary); it uses
the local event spool + OIDC-token fallback.
- `posix` — `sh` runs the bundled `.sh` script. The Docker wrapper's default.
native macOS/Linux Claude Code installs (cargo / release binary) and the
Docker wrapper's checksum-verified host client; it uses the local event
spool + OIDC-token fallback and enforces capture policy v1.
- `posix` — `sh` runs the bundled `.sh` script. This is an explicit
compatibility fallback for the Docker wrapper.
Set `AI_MEMORY_HOOK_PLATFORM` before wiring hooks to override the default.
+7 -4
View File
@@ -414,10 +414,13 @@ Capture policy v1 is enforced by native `ai-memory hook` commands (including
native POSIX/Windows hook commands) and generated OpenCode, OMP, Pi, and
OpenClaw integrations, including the lexical shell-command matching above.
Local installers default to native commands where that
path is supported. Legacy `.sh`/`.ps1` hooks and remote-only/Docker script
bundles do **not** enforce it. Reinstall hooks or refresh/reinstall generated
plugins after upgrading; existing hooks/plugins keep their prior behavior.
Installer capability output describes the selected integration.
path is supported. The Linux/macOS Docker wrapper's ordinary `install-hooks`
path uses its checksum-verified native host client and is supported too.
Legacy `.sh`/`.ps1` hooks, explicit compatibility overrides, and
remote-only/manual Docker script bundles do **not** enforce it. Reinstall hooks
or refresh/reinstall generated plugins after upgrading; existing hooks/plugins
keep their prior behavior. Installer capability output describes the selected
integration.
New clients remain safe with old servers because stripping and dropping happen
on the client. Old clients talking to new servers retain old behavior and cannot
+4 -1
View File
@@ -35,7 +35,10 @@ stdout (or their equivalent context-injection result); Grok and Zero must call
Capture exclusions are separate from MCP registration. Native hook commands and
generated OpenCode/OMP/Pi/OpenClaw integrations enforce `[capture]
ignore_paths`; legacy shell/PowerShell and remote-only/Docker script bundles do
not. Reinstall/refresh an existing hook or plugin to gain it; see
not. The Linux/macOS Docker wrapper's ordinary `install-hooks` command uses its
checksum-verified native host client and is covered; this limitation applies to
manual container/script extraction and explicit compatibility overrides.
Reinstall/refresh an existing hook or plugin to gain it; see
[Capture exclusions](marker-file.md#capture-exclusions).
Claude Desktop, VS Code Copilot, Zed, and Muse Code are **MCP-only** here:
+1
View File
@@ -53,6 +53,7 @@ boundary not yet built.
| 10f | Session-time repair is scope-bound (no cross-project timestamp rewrite) | `ai-memory-store/src/ops.rs` `repair_session_times` — every candidate is read back and updated with `WHERE id=? AND workspace_id=? AND project_id=?`; a row outside that 3-tuple is reported `NotFound` and untouched, same code path as an id that does not exist at all; `ai-memory-mcp/src/admin.rs` `handle_repair_session_times` passes the caller's `(workspace, project)` straight through, never the candidate's own | `ops.rs` `repair_session_times_does_not_touch_a_session_of_another_project` (control: sibling session in-scope repaired in the same batch); `admin_repair_session_times.rs` `cross_project_session_is_not_found_and_untouched`; both bite-checked by dropping the `project_id` filter | STRONG |
| 11a | Hook backpressure (202/429) + bounded fan-out (invariant #5) | `ai-memory-hooks/src/router.rs` semaphore→429, 202 immediately, `MAX_HOOK_BATCH_ITEMS`, bounded LRU limiter | `router.rs` `handle_hook_returns_429_when_ingest_saturated`, `ingest_rate_limiter_is_bounded` | STRONG |
| 11b | Capture exclusions drop before storage | `ai-memory-hooks` `capture_policy.rs` `inspect`→`Drop` (before semaphore/spawn), including shell commands whose arguments name an ignored path (`match_command`; argv elements matched whole and tokenized); an invalid marker makes file and shell calls metadata-only, and the server admits a metadata-only shell body only under an invalid marker (`router.rs` `metadata_protocol_is_legal`); generated OpenCode/OMP/Pi/OpenClaw integrations mirror it in `render_shared.rs` `ts_capture_policy_v1` (`captureMatchCommand`). Candidate/argument flavor (`Flavor::Posix` vs `Flavor::Windows`, used to pick which `ignore_paths` patterns even apply) is derived from the **host** (the cwd), never from the candidate string alone — a POSIX-host candidate spelled with a leading `//` is collapsed to a single `/` before flavor detection (`normalize_candidate`; TS `captureNormalize`'s `windowsHost` parameter, sourced from `captureHostWindows(cwd)`), fixing GHSA-vh98 (a `//`-prefixed candidate used to self-classify as `Flavor::Windows` regardless of host, matching zero POSIX patterns and being captured instead of dropped); a genuine Windows/UNC host (cwd itself windows-flavored) is unaffected | `capture_policy.rs` per-agent `…honors_exclusions` tests, `shell_fixture_vectors` (shared `capture-policy.json` `shell` drop/keep vectors: tool aliases incl. OpenClaw/Devin `exec`, `workdir`, glob directories, Windows paths, and a POSIX-host leading-`//` command via `/{root}/docs/adr/x.md`), `shell_tool_shapes_of_every_adapter_honor_exclusions`, `fixture_vectors` (incl. TS-adapter `bash`/`exec` vectors, and `normalization`'s leading-`//` POSIX vectors alongside the kept Windows-cwd UNC vectors as the no-regression control); `shell_matching_is_off_when_inactive_and_fails_closed_when_invalid_or_over_budget`; `router.rs` `capture_protocol_shell_decisions_survive_server_reinspection`, `capture_protocol_invalid_marker_shell_is_metadata_only` (active metadata-only shell refused, older client's invalid-marker keep stripped, commandless control kept), `capture_protocol_unparseable_marker_strips_shell_events` (server fallback for an unparseable marker), `capture_protocol_invalid_shell_metadata_claim_must_be_canonical` (a stripped shell claim with a path count or non-`extracted` state is refused); `capture_policy.rs` `invalid_marker_strips_shell_calls_with_unparseable_commands`, `long_bash_lc_script_in_argv_is_not_dropped_by_the_match_budget`, **`a_leading_double_slash_candidate_does_not_escape_posix_ignore_paths_via_flavor_mismatch`** (GHSA-vh98 adversarial: attempts the `//repo/secret/...` violation on a POSIX host, proves it now drops, with a plain-single-slash control and a Windows-hosted genuine-UNC control both still correct — fails on the pre-fix code); `hook.rs` `shell_command_reading_an_ignored_path_is_dropped_before_spool`; `render_shared.rs` `generated_capture_policy_v1_node_runtime_evidence` (runs the same fixture sections, including the GHSA-vh98 vectors, against the emitted TypeScript; `#[ignore]` locally, run with `--ignored` under Node 24 by the Linux CI test job) | STRONG |
| 11c | Docker-wrapper capture-policy capability (#1002) | `bin/ai-memory` routes ordinary Linux/macOS `install-hooks` and upgrade refreshes through the stable checksum-verified native host client, so the hook process executes the capture-policy boundary in 11b before spool/network. Only an explicit `AI_MEMORY_HOOK_PLATFORM=posix|windows` retains the documented script compatibility path and its warning; `AI_MEMORY_NATIVE_BIN` may not point back to the wrapper | `scripts/check-native-packaging.sh` `Checking host-launch wrapper routing`: a default allowlist install reaches the fake native client while a Docker executable that always fails proves no container fallback occurred; the explicit `posix` control reaches fake Docker and adds no native invocation; the self-referential native override is refused | STRONG on the shipped Linux/macOS wrapper. Windows and remote/manual script installers remain explicitly outside capture-policy v1 |
| 11c | Capture hook ≤200ms budget (invariant #5) | `hooks/_lib.sh` capture path `curl --max-time 0.2` (context-fetch 1.0s and background drain 2.0s are separate, larger-budget paths) | none (shell-script timeout; hard to unit-test) — watch on any capture-path change | WATCH |
| 11d | Hook server-profile routing: a marker-selected server gets only its own capture and its own token (#992) | `ai-memory-cli/src/server_profiles.rs` `resolve` (validated `ProfileName`, strict `servers.toml` parse, `roots` required once two profiles exist, component-wise root match) and `marker.rs` `find_server_selection` (inherited down the tree, any value shape counts); `commands/hook.rs` `resolve_hook_route` drops a `Rejected` route before spool, handoff and backfill, and hands the drainer no live token for a profile route; `commands/hook_spool.rs` `static_retry_token` (a profile entry retries only with its own stored token), the loopback reroot skip, and chunk splitting on `profile`; generated TS `captureServerRouted` drops a routed repository and gates `fetchHandoff`; `hooks/_lib.sh` `ai_memory_server_routed` (flag refused by `ai_memory_post_hook`/`ai_memory_get_handoff`) and `hooks/lib/ai-memory-hook.ps1` `Test-AiMemoryServerRouted` drop it in the script hooks | `hook.rs` `each_repository_spools_to_its_own_profile_with_its_own_token`, `a_selection_that_does_not_resolve_emits_nothing` (unknown / tokenless / outside roots / roots required / invalid name, plus a resolving control), `session_start_handoff_comes_from_the_profile_server_only`, `a_repository_without_a_server_key_keeps_the_install_default`; `hook_spool.rs` `a_profile_entry_is_never_retried_with_the_install_live_token` (server B accepts exactly the install's live token and must still not get it), `a_profile_entry_recovers_with_its_own_rotated_token` (control), `a_profile_entry_on_a_dead_loopback_port_is_not_rerouted_to_the_default`, `profile_and_default_entries_at_one_address_ride_separate_batches`; `server_profiles.rs` roots/registry/name tests; `marker.rs` `nested_markers_without_server_inherit_the_ancestor_selection`; `install_hooks.rs` `generated_integrations_fail_closed_on_a_server_profile_marker`, `openclaw_plugin.rs` `openclaw_plugin_fails_closed_on_a_server_profile_marker`, and the `server-routed-*` checks in `generated_capture_policy_v1_node_runtime_evidence`; `hook.rs` `an_event_without_a_payload_cwd_routes_by_the_process_cwd`, `a_refused_route_prints_nothing_for_kimi_user_prompts`; `hook_spool.rs` `a_profile_entry_is_not_retried_with_a_token_issued_for_a_new_url`; `server_profiles.rs` `changing_the_url_without_a_token_discards_the_old_token`, `omitted_roots_keep_the_registered_ones`; `marker.rs` `outside_home_the_walk_reaches_a_marker_above_the_checkout_root`, `encoding_noise_cannot_hide_a_server_key`, `an_unreadable_marker_is_a_refused_selection`; `backfill.rs` `a_spawned_backfill_authenticates_like_the_hook_that_spawned_it`; `tests/hooks/test_lib.sh` "server profiles (#992)" section; `hook.rs` `a_mixed_spool_drains_each_event_only_to_its_own_server` (two token-gated servers, one spool, one drain: each server receives exactly its own event with exactly its own bearer); `tests/suite/server_profiles.rs` (the built binary: `server add` → `hook` spools to the profile with its token, unknown profile spools nothing, `uninstall` removes the tokens; `two_real_servers_each_receive_only_their_own_repository` runs two real `ai-memory serve` children with different root tokens and checks on each server which repository landed there); `ai-memory-hooks` `powershell_server_routed.rs` `server_routed_guard_mirrors_the_native_walk` (runs `Test-AiMemoryServerRouted` under real PowerShell: inherited, BOM, look-alike keys, the `$HOME` boundary with its control, past a checkout root outside home, current directory); `tests/hooks/test_lib.sh` and `powershell_home.rs` `a_trailing_separator_on_home_keeps_the_walk_boundary` (a `$HOME` ending in `/` or `\` keeps both script walks at home: a `server` marker above it routes nothing, with a root-home control that does) | STRONG for native hooks, generated TS, `.sh` and `.ps1` hooks. Known gap: an older binary draining a shared spool ignores `profile` |
| 12 | Network/auth posture | `config.rs` loopback `DEFAULT_BIND`; `serve.rs` `validate_http_exposure`, `require_allowed_host`; `auth.rs` `require_bearer` | `serve.rs` host-guard (missing→400 / forged→403), non-loopback-requires-token; `auth.rs` wrong-token 401 | STRONG |
+5 -5
View File
@@ -541,15 +541,15 @@ native on an i7-6700HQ). Notes:
- `windows-bash` — `bash -c` + `.sh` through Git Bash (the previous
default; set this to opt back in, or as a fallback for older Claude Code
builds that do not support exec form).
- `posix` — POSIX `.sh`. The Linux/macOS Docker-wrapper default (the host has
no local binary); set it explicitly to opt a native install back into the
scripts.
- `posix` — POSIX `.sh`. An explicit Linux/macOS Docker-wrapper compatibility
fallback; set it to opt a native install back into the scripts.
- `posix-native` — direct binary call on macOS / Linux (`<exe> hook
--event …`) instead of the `.sh` script, so the hook uses the local event
spool + OIDC-token fallback. The **default for native macOS / Linux
Claude Code installs** (cargo / release binary), mirroring
`windows-native`. The Linux/macOS Docker wrapper forces `posix`, so its
host-rendered config keeps the `.sh` scripts.
`windows-native`. The Linux/macOS Docker wrapper uses its
checksum-verified native host client by default; set `posix` explicitly to
keep the `.sh` scripts.
Set the env var before running `install-hooks` so the chosen platform
is baked into the rendered hook commands.
+23 -2
View File
@@ -88,13 +88,16 @@ main() {
trap cleanup EXIT
log "Checking host-launch wrapper routing"
local fake_docker fake_native wrapper_log
local fake_docker fake_compat_docker fake_native wrapper_log docker_log native_lines
fake_docker="${TMP_ROOT}/forbidden-docker"
fake_compat_docker="${TMP_ROOT}/fake-compat-docker"
fake_native="${TMP_ROOT}/fake-ai-memory"
wrapper_log="${TMP_ROOT}/wrapper.log"
docker_log="${TMP_ROOT}/docker.log"
printf '%s\n' '#!/usr/bin/env bash' 'exit 97' >"${fake_docker}"
printf '%s\n' '#!/usr/bin/env bash' 'printf '\''%s\n'\'' "$*" >>"${AI_MEMORY_DOCKER_TEST_LOG}"' >"${fake_compat_docker}"
printf '%s\n' '#!/usr/bin/env bash' 'printf '\''%s\n'\'' "$*" >>"${AI_MEMORY_WRAPPER_TEST_LOG}"' >"${fake_native}"
chmod 0755 "${fake_docker}" "${fake_native}"
chmod 0755 "${fake_docker}" "${fake_compat_docker}" "${fake_native}"
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory run codex --yolo
@@ -110,11 +113,29 @@ main() {
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory rename-workstream --from typo-nmae --to refactor-db
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory install-hooks --agent claude-code --capture-mode allowlist --apply
assert_contains "${wrapper_log}" "run codex --yolo"
assert_contains "${wrapper_log}" "show --json --no-scan"
assert_contains "${wrapper_log}" "continue --workspace work --yolo"
assert_contains "${wrapper_log}" "workstreams --limit 5 --json"
assert_contains "${wrapper_log}" "rename-workstream --from typo-nmae --to refactor-db"
assert_contains "${wrapper_log}" "install-hooks --agent claude-code --capture-mode allowlist --apply"
native_lines="$(wc -l <"${wrapper_log}")"
AI_MEMORY_DOCKER="${fake_compat_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_HOOK_PLATFORM=posix AI_MEMORY_DOCKER_TEST_LOG="${docker_log}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory install-hooks --agent claude-code --apply
assert_contains "${docker_log}" "install-hooks --agent claude-code --apply"
test "$(wc -l <"${wrapper_log}")" = "${native_lines}" \
|| fail "an explicit posix hook install must retain the container/script path"
if AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="$(pwd)/bin/ai-memory" \
bin/ai-memory run codex 2>/dev/null; then
fail "AI_MEMORY_NATIVE_BIN pointing to the wrapper must be refused instead of recursing"
fi
log "Creating temporary alternate root"
mkdir -p \