Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·3481 lines (3285 loc) · 174 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·3481 lines (3285 loc) · 174 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
set -e
# ─────────────────────────────────────────────────────────────────────
# Agentic Control Plane — Universal Installer
#
# Source: https://github.com/agentic-control-plane/acp-install
# License: MIT
# Docs: https://agenticcontrolplane.com
# Support: https://github.com/agentic-control-plane/acp-install/issues
#
# What this script will do on your machine:
#
# 1. Detect which AI clients are installed
# Claude Code · Cursor · OpenAI Codex CLI · OpenClaw · GitHub Copilot
#
# 1b. SHOW YOU THAT LIST and ask which of them to govern, naming the
# files each one touches, before writing anything at all. Enter
# takes everything found; `n` writes nothing and exits.
#
# 2. Write ~/.acp/govern.mjs (the hook dispatcher script — shared across clients)
#
# 3. Register PreToolUse + PostToolUse hooks in each detected client's config:
# ~/.claude/settings.json (Claude Code)
# ~/.cursor/hooks.json (Cursor)
# ~/.codex/hooks.json (Codex CLI)
# ~/.copilot/hooks/acp.json (GitHub Copilot — CLI and VS Code agent mode)
#
# 4. For Codex specifically:
# - Enable [features].codex_hooks = true in ~/.codex/config.toml
# - Add [mcp_servers.acp] block so `acp_check` / `acp_status` are
# available for explicit questions ("am I allowed to X?")
# - Write ~/.codex/AGENTS.md section confirming hooks already
# govern every tool call (Bash and non-Bash alike)
#
# 5. Open your browser to authenticate and provision an ACP workspace
#
# 6. Save the API key to ~/.acp/credentials (chmod 600)
#
# Usage:
# curl -sf https://agenticcontrolplane.com/install.sh | bash
#
# Skip the question (CI, or you already know what you want):
# … | bash -s -- --all
# … | bash -s -- --only=claude-code,cursor
# With no terminal to ask on, the default is everything detected.
#
# Undo:
# acp-uninstall (removes every file listed below)
#
# Review the source before running:
# curl -sf https://agenticcontrolplane.com/install.sh | less
# github.com/agentic-control-plane/acp-install (mirror)
#
# ─────────────────────────────────────────────────────────────────────
# Everything below runs inside ONE brace group, closed by the final `}`.
# `curl | bash` feeds this script to bash on stdin, and bash executes it as
# it reads it. Any child that reads stdin (pipx, pip, hermes, dsh, npm, muse,
# claude...) then consumes the unread REST OF THE SCRIPT, and bash resumes
# at a random offset: harness canary 2026-10-08 printed the tail of the Muse
# fallback text and died with `syntax error near unexpected token fi` (exit 2)
# right after Hermes installed (gsc#1394). A compound command is parsed in
# full before any of it runs, so by the time a child starts there is nothing
# left on stdin to steal. A truncated download is now a syntax error with no
# side effects, instead of a half-run install.
{
API_BASE="${ACP_API_BASE:-https://api.agenticcontrolplane.com}"
DASHBOARD_BASE="${ACP_DASHBOARD_BASE:-https://cloud.agenticcontrolplane.com}"
SITE_BASE="${ACP_SITE_BASE:-https://agenticcontrolplane.com}"
CONFIG_DIR="$HOME/.acp"
CREDS_FILE="$CONFIG_DIR/credentials"
# ── Local mode: no account, no cloud, nothing leaves your machine ──────
# Enable with: curl -sf …/install.sh | bash -s -- --local (or ACP_LOCAL=1)
LOCAL_MODE=false
for _a in "$@"; do case "$_a" in --local|--no-login) LOCAL_MODE=true ;; esac; done
[ "${ACP_LOCAL:-}" = "1" ] && LOCAL_MODE=true
# ── Update mode: refresh in place, never mint a key ─────────────────────
# What `acp-update` runs. Re-fetches govern.mjs, rewrites every launcher and
# hook registration and directive block for harnesses already detected on
# this machine, then stops before Step 2 (Authenticate) — no device flow,
# no prompt, ~/.acp/credentials is left exactly as it was.
UPDATE_MODE=false
for _a in "$@"; do case "$_a" in --update) UPDATE_MODE=true ;; esac; done
# ── Terminal colors ───────────────────────────────────────────────────
# tput-guarded: green success / red failure / dim secondary, mirroring
# the console's ALLOW/DENY language. Degrades to plain text when stdout
# is not a TTY or the terminal has no color support.
if [ -t 1 ] && command -v tput > /dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
C_GREEN="$(tput setaf 2)"
C_RED="$(tput setaf 1)"
C_DIM="$(tput dim)"
C_RESET="$(tput sgr0)"
else
C_GREEN=""
C_RED=""
C_DIM=""
C_RESET=""
fi
# ── Pinned downloads (acp-install#29) ─────────────────────────────────
# Everything fetched from GitHub and executed later (the govern.mjs hook, the
# harness hooks) is pinned to a commit AND verified against a sha256 embedded
# here, before it is moved into place. A compromised branch, a repo takeover,
# or a TLS-intercepting proxy can no longer turn `main` into persistent code
# execution on every tool call. On any mismatch or fetch failure nothing is
# installed from the network: govern.mjs falls back to the bundled copy below,
# harness hooks print their manual path (the existing fail-open branch).
#
# BUMPING A PIN (after a plugin release): set the ref to the release commit,
# then curl -fsSL https://raw.githubusercontent.com/agentic-control-plane/<repo>/<ref>/<path> | shasum -a 256
# and paste the digest. Hook fixes now reach machines with the installer
# release (or `acp-update`, which re-runs this installer), not on every push.
ACP_PIN_GOVERN_REPO="claude-code-acp-plugin"
# Plugin 0.28.0 (main cb043a8e).
ACP_PIN_GOVERN_REF="cb043a8e81f0d8382dba9e45a4e9ed7ec4c8606c"
ACP_PIN_GOVERN_SHA="1ff4fd77ba62a23a24c81b7dcbd2c6c2c1a71393ff5df2b954c2028b969867a8"
ACP_PIN_PI_REF="e27c427621c6a6c4948e95ca87cfd2c97c27c20d"
ACP_PIN_PI_INDEX_SHA="cf8c50c0ed57c596f965be1a3f8a958d9a8b9b0dc9f8e1377e4286ca39e127c1"
ACP_PIN_PRIME_REF="4043829b9acdaa2d258ca5237166fcc7910bfd14"
ACP_PIN_PRIME_INDEX_SHA="d5090db76007e8ebb6f689ed2c7ee55a272fcdb0081617f0deb9c75d8957660d"
ACP_PIN_GROK_REF="4710fc511d07346468758db780fafabf09cedede"
ACP_PIN_GROK_HOOK_SHA="60b6fbe278066d6e7cdaaea77a353204756b06dd44816e649336e9888896d28c"
ACP_PIN_GROK_JSON_SHA="7e5edd4a8615523548ce660d84fde45ef480ac26bda7fca53ecfd0bfcf011b27"
ACP_PIN_AGY_REF="3fe9c68caa12ab5d37fcdc6f48ca6304cc0e04c7"
ACP_PIN_AGY_HOOK_SHA="ef0e660b41a287f4da41a8770de757c4791dd089546f7ffbd9e85f9373acac8a"
ACP_PIN_AGY_JSON_SHA="41b5e3b56e2d4c5f7ff221213dba96df636c5bfebeb085b42db16a23e30008ae"
# sha256 of a file on stdout; empty (and nonzero) when no tool is available.
_acp_sha256() {
if command -v shasum > /dev/null 2>&1; then shasum -a 256 "$1" 2>/dev/null | cut -d' ' -f1
elif command -v sha256sum > /dev/null 2>&1; then sha256sum "$1" 2>/dev/null | cut -d' ' -f1
elif command -v openssl > /dev/null 2>&1; then openssl dgst -sha256 "$1" 2>/dev/null | sed 's/^.*= *//'
elif command -v node > /dev/null 2>&1; then node -e 'const c=require("crypto"),f=require("fs");process.stdout.write(c.createHash("sha256").update(f.readFileSync(process.argv[1])).digest("hex"))' "$1" 2>/dev/null
else return 1
fi
}
# _acp_fetch_pinned URL EXPECTED_SHA256 DEST
# Download to a private temp file in DEST's own directory, hash THAT file, and
# only then rename it over DEST (same filesystem, so the rename is atomic and
# the bytes that were hashed are the bytes that land). Returns 0 only on a
# verified install. On failure DEST is untouched and ACP_PIN_FAIL says why:
# "download" (offline / 404 / no temp file) or "checksum" (bytes differ).
ACP_PIN_FAIL=""
_acp_fetch_pinned() {
_pf_url="$1"; _pf_want="$2"; _pf_dest="$3"
ACP_PIN_FAIL="download"
_pf_tmp="$(mktemp "$_pf_dest.XXXXXX" 2>/dev/null)" || return 1
if ! curl -fsSL --max-time 20 "$_pf_url" -o "$_pf_tmp" 2>/dev/null || [ ! -s "$_pf_tmp" ]; then
rm -f "$_pf_tmp"; return 1
fi
_pf_got="$(_acp_sha256 "$_pf_tmp")"
if [ -z "$_pf_got" ] || [ "$_pf_got" != "$_pf_want" ]; then
ACP_PIN_FAIL="checksum"
rm -f "$_pf_tmp"; return 1
fi
chmod 644 "$_pf_tmp" 2>/dev/null || true
mv -f "$_pf_tmp" "$_pf_dest" || { rm -f "$_pf_tmp"; ACP_PIN_FAIL="download"; return 1; }
ACP_PIN_FAIL=""
return 0
}
# One-line warning when a pinned download was REJECTED (not merely offline).
_acp_pin_warn() {
[ "$ACP_PIN_FAIL" = "checksum" ] || return 0
echo " ${C_RED}[ACP] WARNING:${C_RESET} $1 did not match its pinned sha256 — NOT installed. $2" >&2
}
# ── Detect available clients ──────────────────────────────────────────
HAS_CLAUDE=false
HAS_CURSOR=false
HAS_CODEX=false
HAS_OPENCLAW=false
HAS_OPENCODE=false
INSTALLED=""
if [ -d "$HOME/.claude" ] || command -v claude > /dev/null 2>&1; then
HAS_CLAUDE=true
fi
if [ -d "$HOME/.cursor" ] || command -v cursor > /dev/null 2>&1; then
HAS_CURSOR=true
fi
if [ -d "$HOME/.codex" ] || command -v codex > /dev/null 2>&1; then
HAS_CODEX=true
fi
if command -v openclaw > /dev/null 2>&1; then
HAS_OPENCLAW=true
fi
# Every remaining harness is detected HERE, before a single byte is written,
# because the consent gate below needs the whole list in front of it. Each
# one's install block still lives further down next to its own notes; the
# gate reaches them by clearing HAS_* for anything the user didn't pick.
# Hermes Agent — governed via its native pip plugin.
HAS_HERMES=false
if command -v hermes > /dev/null 2>&1; then
HAS_HERMES=true
fi
# DeepSeek Harness — native plugin, per profile.
HAS_DSH=false
DSH_HOME_DIR="${DSH_HOME:-$HOME/.dsh}"
if command -v dsh > /dev/null 2>&1 || [ -d "$DSH_HOME_DIR/profiles" ]; then
HAS_DSH=true
fi
# pi (earendil-works) — a global extension file. The ~/.pi/agent dir is the
# reliable signal; `command -v pi` alone can match an unrelated binary named
# pi, so require the dir when falling back to PATH.
HAS_PI=false
PI_EXT_DIR="$HOME/.pi/agent/extensions"
if [ -d "$HOME/.pi/agent" ] || { command -v pi > /dev/null 2>&1 && [ -d "$HOME/.pi" ]; }; then
HAS_PI=true
fi
# Prime Agent — same shape as pi; require the dir on the PATH fallback.
HAS_PRIME=false
PRIME_EXT_DIR="$HOME/.prime/agent/extensions"
if [ -d "$HOME/.prime/agent" ] || { command -v prime-agent > /dev/null 2>&1 && [ -d "$HOME/.prime" ]; }; then
HAS_PRIME=true
fi
# Muse Code — plugin bundle; require the config dir on the PATH fallback.
HAS_MUSE=false
if [ -d "$HOME/.config/muse" ] || { command -v muse > /dev/null 2>&1 && [ -x "$HOME/.local/bin/muse" ]; }; then
HAS_MUSE=true
fi
# Grok Build — purpose-built hook in ~/.grok/hooks.
HAS_GROK=false
if [ -d "$HOME/.grok" ] || command -v grok > /dev/null 2>&1; then
HAS_GROK=true
fi
# Google Antigravity (agy) — one shared registration for CLI, IDE and app.
HAS_AGY=false
if command -v agy > /dev/null 2>&1 || [ -d "$HOME/.gemini/antigravity-cli" ]; then
HAS_AGY=true
fi
# opencode (sst/opencode) — global config/plugins under ~/.config/opencode.
if [ -d "$HOME/.config/opencode" ] || command -v opencode > /dev/null 2>&1; then
HAS_OPENCODE=true
fi
# Qwen Code (QwenLM/qwen-code, Gemini CLI lineage) — hooks are Claude Code's
# contract (PreToolUse/PostToolUse, hookSpecificOutput.permissionDecision,
# exit 2 = block) read from ~/.qwen/settings.json; timeouts are milliseconds.
HAS_QWEN=false
if [ -d "$HOME/.qwen" ] || command -v qwen > /dev/null 2>&1; then
HAS_QWEN=true
fi
# GitHub Copilot — the CLI (~/.copilot, `copilot` on PATH) and VS Code's
# agent mode (the github.copilot-chat extension) read ONE user-level hook
# directory, ~/.copilot/hooks/, in Claude Code's hook contract. Either
# presence is enough: the file serves both.
HAS_COPILOT=false
if [ -d "$HOME/.copilot" ] || command -v copilot > /dev/null 2>&1; then
HAS_COPILOT=true
fi
for _vsx in "$HOME/.vscode/extensions" "$HOME/.vscode-insiders/extensions" "$HOME/.vscode-server/extensions"; do
if ls -d "$_vsx"/github.copilot-chat-* > /dev/null 2>&1; then HAS_COPILOT=true; fi
done
# ── Consent: what we found, and what you actually want governed ────────
#
# Two people signed up on 2026-09-17, looked at what had just been written
# to their machines, and spent the rest of their session taking it back off.
# Neither was ever asked. An installer that wires every agent tool on the
# box without showing you the list first reads like something you caught
# rather than something you chose — so show the list, say plainly what each
# entry means, and let people pick.
#
# Everything detected is the default, because governing one harness and not
# another leaves a hole. A default, though, not a fait accompli.
HARNESS_SLUG=(); HARNESS_NAME=(); HARNESS_FILES=(); HARNESS_NOTE=()
_offer() { # slug detected Label files note
if [ "$2" != true ]; then return 0; fi
HARNESS_SLUG+=("$1"); HARNESS_NAME+=("$3"); HARNESS_FILES+=("$4"); HARNESS_NOTE+=("$5")
}
_offer claude-code "$HAS_CLAUDE" "Claude Code" \
"~/.claude/settings.json · ~/.claude/CLAUDE.md" \
"Approval prompts appear inline in your session. Adds a claude-acp launcher that prices model calls."
_offer cursor "$HAS_CURSOR" "Cursor" \
"~/.cursor/hooks.json · ~/.cursor/mcp.json" \
"Covers tool calls made by Cursor's agent, not your own editing."
_offer codex "$HAS_CODEX" "Codex CLI" \
"~/.codex/config.toml · ~/.codex/AGENTS.md" \
"Codex has no approval prompt, so a policy that would ask blocks instead. Enables Codex's hooks feature."
_offer openclaw "$HAS_OPENCLAW" "OpenClaw" \
"~/.openclaw/settings.json" \
"Hook on every tool call."
_offer opencode "$HAS_OPENCODE" "opencode" \
"~/.config/opencode/" \
"Hook on every tool call, via its plugin system."
_offer qwen-code "$HAS_QWEN" "Qwen Code" \
"~/.qwen/settings.json" \
"Hook on every tool call; reads Claude Code's hook contract."
_offer copilot "$HAS_COPILOT" "GitHub Copilot" \
"~/.copilot/hooks/acp.json" \
"One file for Copilot CLI and VS Code agent mode; reads Claude Code's hook contract. Cloud coding-agent runs need the repo-level copy (see the guide)."
_offer hermes "$HAS_HERMES" "Hermes Agent" \
"its pip environment (acp-hermes)" \
"Installs a Python package into the environment that runs hermes."
_offer dsh "$HAS_DSH" "DeepSeek Harness" \
"$DSH_HOME_DIR/profiles" \
"Adds a plugin to each profile via its own plugin manager."
_offer pi "$HAS_PI" "pi" \
"$PI_EXT_DIR/acp.ts" \
"A global extension file, so governance is on before any repo is opened."
_offer prime-agent "$HAS_PRIME" "Prime Agent" \
"$PRIME_EXT_DIR/acp.ts" \
"A global extension file, loaded without a project-trust prompt."
_offer muse "$HAS_MUSE" "Muse Code" \
"~/.config/muse/" \
"Plugin bundle. Its background agent makes tool calls of its own, which will appear in your audit."
_offer grok "$HAS_GROK" "Grok Build" \
"~/.grok/hooks/acp.json" \
"Purpose-built hook. Grok also auto-loads Claude-format hooks, so this is the one that actually decides."
_offer agy "$HAS_AGY" "Antigravity" \
"~/.gemini/config/hooks.json" \
"One registration shared by its CLI, IDE and app. Merged under our own key, never overwriting yours."
SELECTED=""
_selected() { case " $SELECTED " in *" $1 "*) return 0;; esac; return 1; }
# Is this harness ALREADY governed on this machine? Used only by --update,
# to tell "refresh what you run" apart from "adopt something new". Each probe
# looks for the artifact that harness's own install block writes, so a
# half-finished install reads as not-wired and gets repaired rather than
# skipped. Only ever called from an `if`, so a false return is a verdict.
_already_wired() {
case "$1" in
claude-code) [ -f "$HOME/.claude/settings.json" ] && grep -q 'govern' "$HOME/.claude/settings.json" 2>/dev/null ;;
cursor) [ -f "$HOME/.cursor/hooks.json" ] && grep -q 'govern' "$HOME/.cursor/hooks.json" 2>/dev/null ;;
codex) [ -f "$HOME/.codex/config.toml" ] && grep -q 'acp' "$HOME/.codex/config.toml" 2>/dev/null ;;
openclaw) [ -f "$HOME/.openclaw/settings.json" ] && grep -q 'govern' "$HOME/.openclaw/settings.json" 2>/dev/null ;;
opencode) [ -f "$HOME/.config/opencode/opencode.json" ] && grep -q 'acp' "$HOME/.config/opencode/opencode.json" 2>/dev/null ;;
qwen-code) [ -f "$HOME/.qwen/settings.json" ] && grep -q 'govern' "$HOME/.qwen/settings.json" 2>/dev/null ;;
copilot) [ -f "$HOME/.copilot/hooks/acp.json" ] && grep -q 'govern' "$HOME/.copilot/hooks/acp.json" 2>/dev/null ;;
hermes) [ -f "$HOME/.hermes/SOUL.md" ] && grep -q 'acp:begin' "$HOME/.hermes/SOUL.md" 2>/dev/null ;;
dsh) [ -f "$DSH_HOME_DIR/settings.yaml" ] && grep -q 'agenticcontrolplane' "$DSH_HOME_DIR/settings.yaml" 2>/dev/null ;;
pi) [ -f "$PI_EXT_DIR/acp.ts" ] ;;
prime-agent) [ -f "$PRIME_EXT_DIR/acp.ts" ] ;;
muse) [ -f "$HOME/.config/muse/AGENTS.md" ] && grep -q 'acp:begin' "$HOME/.config/muse/AGENTS.md" 2>/dev/null ;;
grok) [ -f "$HOME/.grok/hooks/acp.json" ] ;;
agy) [ -f "$HOME/.gemini/config/hooks.json" ] && grep -q 'acp' "$HOME/.gemini/config/hooks.json" 2>/dev/null ;;
*) return 1 ;;
esac
}
_select_all() { SELECTED=""; for _s in "${HARNESS_SLUG[@]}"; do SELECTED="$SELECTED $_s"; done; }
# Non-interactive selection, for CI and for anyone who'd rather not be asked:
# --only=claude-code,cursor --all ACP_ONLY=claude-code
ACP_ONLY="${ACP_ONLY:-}"
for _a in "$@"; do
case "$_a" in
--only=*) ACP_ONLY="${_a#--only=}" ;;
--all|--yes|-y) ACP_ONLY="all" ;;
esac
done
_parse_pick() {
local _raw _tok _idx _s _found
_raw="$(printf '%s' "$1" | tr ',' ' ')"
SELECTED=""
for _tok in $_raw; do
case "$_tok" in
[0-9]*)
_idx=$((_tok - 1))
if [ "$_idx" -ge 0 ] && [ "$_idx" -lt "${#HARNESS_SLUG[@]}" ]; then
SELECTED="$SELECTED ${HARNESS_SLUG[$_idx]}"
else
echo " ${C_DIM}Ignoring \"$_tok\" — there is no item $_tok.${C_RESET}"
fi
;;
*)
_found=false
for _s in "${HARNESS_SLUG[@]}"; do
if [ "$_s" = "$_tok" ]; then SELECTED="$SELECTED $_s"; _found=true; fi
done
if [ "$_found" != true ]; then
echo " ${C_DIM}Ignoring \"$_tok\" — not one of the tools found here.${C_RESET}"
fi
;;
esac
done
}
if [ "${#HARNESS_SLUG[@]}" -gt 0 ] && [ "$UPDATE_MODE" = false ]; then
echo ""
echo " Agentic Control Plane"
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if [ "${#HARNESS_SLUG[@]}" -eq 1 ]; then
echo " Found one agent tool on this machine:"
else
echo " Found ${#HARNESS_SLUG[@]} agent tools on this machine:"
fi
echo ""
for _i in "${!HARNESS_SLUG[@]}"; do
printf " %d) %s\n" "$((_i + 1))" "${HARNESS_NAME[$_i]}"
printf " %s\n" "${C_DIM}writes: ${HARNESS_FILES[$_i]}${C_RESET}"
printf " %s\n" "${C_DIM}${HARNESS_NOTE[$_i]}${C_RESET}"
done
echo ""
echo " For each one you pick, ACP adds a hook that runs before and after"
echo " every tool call your agent makes. The hook records the call — tool"
echo " name, a truncated preview of the arguments and the result, and the"
echo " decision — to your workspace, and can ask or block according to your"
echo " policy. Nothing is blocked until you write a policy. Your agents keep"
echo " working if ACP is unreachable."
echo ""
echo " ${C_DIM}Shared, once: ~/.acp/ plus one marked line adding ~/.acp/bin to PATH"
echo " in your shell rc. We append inside acp:begin/acp:end markers and never"
echo " overwrite what you wrote.${C_RESET}"
echo ""
echo " ${C_DIM}Remove all of it: acp-uninstall${C_RESET}"
echo " ${C_DIM}What we store: https://agenticcontrolplane.com/trust${C_RESET}"
echo ""
if [ "$ACP_ONLY" = "all" ]; then
_select_all
echo " Governing all of them."
elif [ -n "$ACP_ONLY" ]; then
_parse_pick "$ACP_ONLY"
elif [ -r /dev/tty ]; then
printf " Which should ACP govern? [Enter = all · 1,3 = pick · n = none]: "
_reply=""
read -r _reply < /dev/tty || _reply=""
echo ""
case "$_reply" in
"") _select_all ;;
n|N|no|NO|none|q|Q|quit)
echo " Nothing installed. Nothing was written to this machine."
echo ""
exit 0
;;
*) _parse_pick "$_reply" ;;
esac
else
_select_all
echo " ${C_DIM}No terminal to ask on — governing all of the above. Narrow it with"
echo " --only=<names>, or remove everything later with acp-uninstall.${C_RESET}"
fi
if [ -z "$SELECTED" ]; then
echo " Nothing selected, so nothing was written to this machine."
echo " Re-run and pick by number, or: --only=claude-code"
echo ""
exit 0
fi
# The gate reaches every install block below through these flags alone —
# each block keeps its own logic and its own notes.
if ! _selected claude-code; then HAS_CLAUDE=false; fi
if ! _selected cursor; then HAS_CURSOR=false; fi
if ! _selected codex; then HAS_CODEX=false; fi
if ! _selected openclaw; then HAS_OPENCLAW=false; fi
if ! _selected opencode; then HAS_OPENCODE=false; fi
if ! _selected qwen-code; then HAS_QWEN=false; fi
if ! _selected copilot; then HAS_COPILOT=false; fi
if ! _selected hermes; then HAS_HERMES=false; fi
if ! _selected dsh; then HAS_DSH=false; fi
if ! _selected pi; then HAS_PI=false; fi
if ! _selected prime-agent; then HAS_PRIME=false; fi
if ! _selected muse; then HAS_MUSE=false; fi
if ! _selected grok; then HAS_GROK=false; fi
if ! _selected agy; then HAS_AGY=false; fi
elif [ "${#HARNESS_SLUG[@]}" -gt 0 ] && [ "$UPDATE_MODE" = true ]; then
# An update refreshes what you already run. It does NOT adopt a harness
# you never chose. Observed 2026-09-17: `acp-update` on a machine wired
# for Claude Code, Codex and opencode silently installed governance into
# pi and Antigravity as well, because they had appeared on the box since
# the original install. Refreshing what is wired is not a new decision;
# taking over a new harness is, and an update is the last place someone
# expects to be asked to make one. So: name them, touch nothing, and say
# exactly how to opt in.
_new_found=""
for _i in "${!HARNESS_SLUG[@]}"; do
if ! _already_wired "${HARNESS_SLUG[$_i]}"; then
_new_found="${_new_found:+$_new_found }${HARNESS_SLUG[$_i]}"
case "${HARNESS_SLUG[$_i]}" in
claude-code) HAS_CLAUDE=false ;;
cursor) HAS_CURSOR=false ;;
codex) HAS_CODEX=false ;;
openclaw) HAS_OPENCLAW=false ;;
opencode) HAS_OPENCODE=false ;;
qwen-code) HAS_QWEN=false ;;
copilot) HAS_COPILOT=false ;;
hermes) HAS_HERMES=false ;;
dsh) HAS_DSH=false ;;
pi) HAS_PI=false ;;
prime-agent) HAS_PRIME=false ;;
muse) HAS_MUSE=false ;;
grok) HAS_GROK=false ;;
agy) HAS_AGY=false ;;
esac
fi
done
if [ -n "$_new_found" ]; then
echo ""
echo " New on this machine since you installed ACP:${C_RESET}"
for _s in $_new_found; do
for _i in "${!HARNESS_SLUG[@]}"; do
if [ "${HARNESS_SLUG[$_i]}" = "$_s" ]; then
echo " ${HARNESS_NAME[$_i]} ${C_DIM}(${HARNESS_FILES[$_i]})${C_RESET}"
fi
done
done
echo " ${C_DIM}Left alone — an update refreshes what you already run, it doesn't"
echo " take over something new. To govern them too:${C_RESET}"
echo " curl -sf https://agenticcontrolplane.com/install.sh | bash -s -- --only=$(echo "$_new_found" | tr ' ' ',')"
echo ""
fi
fi
# Hermes Agent — governed via its native pip plugin. One front door: this
# installer performs the plugin install itself (David 2026-07-21: fold every
# harness into the one-liner). Fail-open: any step failing degrades to
# printing the manual commands, never a broken half-install.
# Cloud-only: acp-hermes governs via a workspace token (without one it passes
# through), so --local skips the plugin install rather than wiring a no-op.
if [ "$HAS_HERMES" = true ] && [ "$LOCAL_MODE" = true ]; then
echo " ${C_DIM}Hermes detected — skipped in --local mode (its ACP plugin needs a workspace; local decisions aren't wired there yet).${C_RESET}"
echo ""
fi
if [ "$HAS_HERMES" = true ] && [ "$LOCAL_MODE" = false ]; then
echo " Detected Hermes Agent — installing the ACP plugin…"
HERMES_PLUGIN_OK=false
# If hermes lives in a pipx venv, the plugin must be injected there —
# a bare pip install lands in the wrong environment, and pipx venvs ship
# without pip, so the shebang fallback below can't reach them either.
# The venv is named after the PyPI package (`hermes-agent`, not `hermes`):
# read the real name off `pipx list` ("package hermes-agent 0.19.0, ...")
# instead of guessing it from the command name.
if command -v pipx > /dev/null 2>&1; then
HERMES_PIPX_VENV="$(pipx list 2>/dev/null | sed -n 's/^[[:space:]]*package[[:space:]]\{1,\}\(hermes[^[:space:],]*\).*/\1/p' | head -1)"
if [ -n "$HERMES_PIPX_VENV" ]; then
pipx inject "$HERMES_PIPX_VENV" acp-hermes --force >/dev/null 2>&1 && HERMES_PLUGIN_OK=true
fi
fi
# Otherwise install with the same interpreter that runs hermes (shebang),
# falling back to plain pip. PEP 668 boxes will refuse — that's the
# fail-open path below, not something to --break-system-packages through.
if [ "$HERMES_PLUGIN_OK" = false ]; then
HERMES_BIN="$(command -v hermes)"
HERMES_PY="$(head -1 "$HERMES_BIN" 2>/dev/null | sed -n 's/^#!//p')"
if [ -n "$HERMES_PY" ] && [ -x "$HERMES_PY" ]; then
"$HERMES_PY" -m pip install --upgrade acp-hermes >/dev/null 2>&1 && HERMES_PLUGIN_OK=true
fi
fi
if [ "$HERMES_PLUGIN_OK" = false ]; then
pip install --upgrade acp-hermes >/dev/null 2>&1 && HERMES_PLUGIN_OK=true
fi
if [ "$HERMES_PLUGIN_OK" = true ] && hermes plugins enable acp >/dev/null 2>&1; then
echo " ${C_GREEN}✓ Hermes governed${C_RESET} — next: acp-hermes login (headless box: set ACP_BEARER_TOKEN instead)"
INSTALLED="${INSTALLED:+$INSTALLED, }Hermes"
else
echo " Couldn't install automatically (often a PEP 668 managed environment)."
echo " Run it in your Python env of choice:"
echo " pip install acp-hermes && hermes plugins enable acp && acp-hermes login"
echo " Guide: https://agenticcontrolplane.com/integrations/hermes"
fi
echo ""
fi
# DeepSeek Harness (dsh) — governed via its native plugin system, same
# one-front-door rule as Hermes (David 2026-07-21: fold every harness into
# the one-liner). Plugins are profile-scoped, so install into every existing
# profile; `dsh plugin add` is idempotent (pnpm re-links the same package).
# Fail-open: any refusal degrades to printing the manual command.
# Cloud-only like Hermes: @agenticcontrolplane/dsh needs a workspace credential; local
# decisions aren't wired there yet.
if [ "$HAS_DSH" = true ] && [ "$LOCAL_MODE" = true ]; then
echo " ${C_DIM}DeepSeek Harness detected — skipped in --local mode (its ACP plugin needs a workspace; local decisions aren't wired there yet).${C_RESET}"
echo ""
fi
if [ "$HAS_DSH" = true ] && [ "$LOCAL_MODE" = false ]; then
echo " Detected DeepSeek Harness — installing the ACP plugin…"
DSH_PROFILES_INSTALLED=0
DSH_PROFILES_FAILED=0
if command -v dsh > /dev/null 2>&1 && [ -d "$DSH_HOME_DIR/profiles" ]; then
for _dsh_prof in "$DSH_HOME_DIR"/profiles/*/; do
[ -d "$_dsh_prof" ] || continue
_dsh_prof_name="$(basename "$_dsh_prof")"
# The installation's shared node_modules dir is not a profile.
[ "$_dsh_prof_name" = "node_modules" ] && continue
if dsh plugin --profile "$_dsh_prof_name" add @agenticcontrolplane/dsh >/dev/null 2>&1; then
DSH_PROFILES_INSTALLED=$((DSH_PROFILES_INSTALLED + 1))
else
DSH_PROFILES_FAILED=$((DSH_PROFILES_FAILED + 1))
fi
done
fi
if [ "$DSH_PROFILES_INSTALLED" -gt 0 ] && [ "$DSH_PROFILES_FAILED" -eq 0 ]; then
echo " ${C_GREEN}✓ DeepSeek Harness governed${C_RESET} — plugin added to $DSH_PROFILES_INSTALLED profile(s); active from the next dsh boot."
INSTALLED="${INSTALLED:+$INSTALLED, }DeepSeek Harness"
else
# No profiles yet, dsh not on PATH, or an add refused (dsh needs Node 22 —
# a Node 20 default breaks it). Print the manual path instead of guessing.
echo " Couldn't finish automatically (no profiles yet, or dsh isn't on PATH here — note dsh needs Node 22)."
echo " Run it against the profile you use:"
echo " dsh plugin --profile <your-profile> add @agenticcontrolplane/dsh"
echo " Guide: https://github.com/agentic-control-plane/dsh-acp-plugin"
fi
echo ""
fi
# pi (earendil-works) — ships no permission system by design; governed via a
# native TypeScript extension on its tool_call / tool_result events. Same
# one-front-door rule. Extensions are files under ~/.pi/agent/extensions/;
# the global path loads without a project-trust prompt, so governance is on
# before any repo is opened. Fail-open: a fetch failure prints the manual path.
# Cloud-only like Hermes/dsh: the extension reads ~/.acp/credentials; local
# decisions aren't wired there yet.
if [ "$HAS_PI" = true ] && [ "$LOCAL_MODE" = true ]; then
echo " ${C_DIM}pi detected — skipped in --local mode (its ACP extension needs a workspace; local decisions aren't wired there yet).${C_RESET}"
echo ""
fi
if [ "$HAS_PI" = true ] && [ "$LOCAL_MODE" = false ]; then
echo " Detected pi — installing the ACP extension…"
PI_EXT_OK=false
if mkdir -p "$PI_EXT_DIR" 2>/dev/null && _acp_fetch_pinned \
"https://raw.githubusercontent.com/agentic-control-plane/pi-acp-plugin/$ACP_PIN_PI_REF/index.ts" \
"$ACP_PIN_PI_INDEX_SHA" "$PI_EXT_DIR/acp.ts"; then
PI_EXT_OK=true
else
_acp_pin_warn "the pi extension" "Skipping it."
fi
if [ "$PI_EXT_OK" = true ]; then
echo " ${C_GREEN}✓ pi governed${C_RESET} — extension written to ~/.pi/agent/extensions/acp.ts; active from the next pi session (needs Node 22)."
INSTALLED="${INSTALLED:+$INSTALLED, }pi"
else
# Network refused or the extensions dir isn't writable — print the manual
# path rather than guessing.
echo " Couldn't fetch automatically. Install it by hand:"
echo " mkdir -p ~/.pi/agent/extensions && curl -sf https://raw.githubusercontent.com/agentic-control-plane/pi-acp-plugin/$ACP_PIN_PI_REF/index.ts -o ~/.pi/agent/extensions/acp.ts # expected sha256: $ACP_PIN_PI_INDEX_SHA"
echo " Guide: https://agenticcontrolplane.com/integrations/pi"
fi
echo ""
fi
# Prime Agent (Prime Intellect) — a pi-mono hard fork that kept pi's extension
# API; governed via the same kind of native TypeScript extension on its
# tool_call / tool_result events (prime-agent-acp-plugin adds an argv-based
# headless fix for prime's hasUI print-mode bug). Same one-front-door rule.
# Extensions under ~/.prime/agent/extensions/ load without a project-trust
# prompt. Fail-open: a fetch failure prints the manual path.
# Cloud-only like pi: the extension reads ~/.acp/credentials; local decisions
# aren't wired there yet.
if [ "$HAS_PRIME" = true ] && [ "$LOCAL_MODE" = true ]; then
echo " ${C_DIM}Prime Agent detected — skipped in --local mode (its ACP extension needs a workspace; local decisions aren't wired there yet).${C_RESET}"
echo ""
fi
if [ "$HAS_PRIME" = true ] && [ "$LOCAL_MODE" = false ]; then
echo " Detected Prime Agent — installing the ACP extension…"
PRIME_EXT_OK=false
if mkdir -p "$PRIME_EXT_DIR" 2>/dev/null && _acp_fetch_pinned \
"https://raw.githubusercontent.com/agentic-control-plane/prime-agent-acp-plugin/$ACP_PIN_PRIME_REF/index.ts" \
"$ACP_PIN_PRIME_INDEX_SHA" "$PRIME_EXT_DIR/acp.ts"; then
PRIME_EXT_OK=true
else
_acp_pin_warn "the Prime Agent extension" "Skipping it."
fi
if [ "$PRIME_EXT_OK" = true ]; then
echo " ${C_GREEN}✓ Prime Agent governed${C_RESET} — extension written to ~/.prime/agent/extensions/acp.ts; active from the next session or /reload (needs Node 22.8+)."
else
# Network refused or the extensions dir isn't writable — print the manual
# path rather than guessing.
echo " Couldn't fetch automatically. Install it by hand:"
echo " mkdir -p ~/.prime/agent/extensions && curl -sf https://raw.githubusercontent.com/agentic-control-plane/prime-agent-acp-plugin/$ACP_PIN_PRIME_REF/index.ts -o ~/.prime/agent/extensions/acp.ts # expected sha256: $ACP_PIN_PRIME_INDEX_SHA"
echo " Guide: https://agenticcontrolplane.com/integrations/prime-agent"
fi
echo ""
fi
# Muse Code (Meta) — governed via its native plugin system (behind the
# MUSE_EXPERIMENTAL_PLUGINS flag in the 0.2.1 beta; the flag gates only the
# management CLI, approved hooks fire without it). Our npm package IS the
# plugin bundle. Same one-front-door rule as Hermes/dsh/pi. Fail-open: any
# refusal degrades to printing the manual commands.
# Cloud-only like Hermes/dsh/pi: Muse clears the hook environment, so the
# plugin reads ~/.acp/credentials; local decisions aren't wired there yet.
if [ "$HAS_MUSE" = true ] && [ "$LOCAL_MODE" = true ]; then
echo " ${C_DIM}Muse Code detected — skipped in --local mode (its ACP plugin needs a workspace; local decisions aren't wired there yet).${C_RESET}"
echo ""
fi
if [ "$HAS_MUSE" = true ] && [ "$LOCAL_MODE" = false ]; then
echo " Detected Muse Code — installing the ACP plugin…"
MUSE_PLUGIN_OK=false
# npm global install, then install+approve the bundle into Muse. The
# experimental flag is required for the plugins CLI; approved hooks then run
# without it. All three steps must succeed to claim governed.
if npm install -g @agenticcontrolplane/muse-code >/dev/null 2>&1; then
_muse_pkg="$(npm root -g 2>/dev/null)/@agenticcontrolplane/muse-code"
if [ -d "$_muse_pkg" ] \
&& MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins install "$_muse_pkg" --scope user >/dev/null 2>&1 \
&& MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins approve acp >/dev/null 2>&1; then
MUSE_PLUGIN_OK=true
fi
fi
if [ "$MUSE_PLUGIN_OK" = true ]; then
echo " ${C_GREEN}✓ Muse Code governed${C_RESET} — plugin installed and approved; active from the next Muse session."
INSTALLED="${INSTALLED:+$INSTALLED, }Muse Code"
else
# npm/muse not on PATH here, or an install/approve step refused — print the
# manual commands rather than guessing.
echo " Couldn't finish automatically (npm or muse not on PATH here, or a step refused). Run:"
echo " npm install -g @agenticcontrolplane/muse-code"
echo " MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins install \"\$(npm root -g)/@agenticcontrolplane/muse-code\" --scope user"
echo " MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins approve acp"
echo " Guide: https://agenticcontrolplane.com/integrations/muse-code"
fi
echo ""
fi
# Grok Build (xAI) — hooks register user-globally in ~/.grok/hooks/*.json (no
# trust prompt). Two raw-file fetches: the hook itself into ~/.acp/hooks and
# the registration JSON into ~/.grok/hooks. NOTE: Grok also auto-loads hooks
# from ~/.claude/settings.json but doesn't parse Claude's output vocabulary,
# so the Claude hook alone would fire and be ignored — this purpose-built hook
# is the governed path. Fail-open: any refusal prints the manual commands.
# Cloud-only like Hermes/dsh/pi/Muse: the hook reads ~/.acp/credentials.
if [ "$HAS_GROK" = true ] && [ "$LOCAL_MODE" = true ]; then
echo " ${C_DIM}Grok Build detected — skipped in --local mode (its ACP hook needs a workspace; local decisions aren't wired there yet).${C_RESET}"
echo ""
fi
if [ "$HAS_GROK" = true ] && [ "$LOCAL_MODE" = false ]; then
echo " Detected Grok Build — installing the ACP hook…"
GROK_HOOK_OK=false
_grok_raw="https://raw.githubusercontent.com/agentic-control-plane/grok-build-acp-plugin/$ACP_PIN_GROK_REF"
if mkdir -p "$HOME/.acp/hooks/grok-build" "$HOME/.grok/hooks" 2>/dev/null \
&& _acp_fetch_pinned "$_grok_raw/hook.mjs" "$ACP_PIN_GROK_HOOK_SHA" "$HOME/.acp/hooks/grok-build/hook.mjs" \
&& _acp_fetch_pinned "$_grok_raw/hooks/acp.json" "$ACP_PIN_GROK_JSON_SHA" "$HOME/.grok/hooks/acp.json"; then
GROK_HOOK_OK=true
else
_acp_pin_warn "a Grok Build hook file" "Hook not registered."
fi
if [ "$GROK_HOOK_OK" = true ]; then
echo " ${C_GREEN}✓ Grok Build governed${C_RESET} — hook registered user-globally; fires in every mode, always-approve included; active from the next Grok session."
INSTALLED="${INSTALLED:+$INSTALLED, }Grok Build"
else
echo " Couldn't finish automatically (curl refused or a directory wasn't writable). Run:"
echo " mkdir -p ~/.acp/hooks/grok-build ~/.grok/hooks"
echo " curl -fsSL $_grok_raw/hook.mjs -o ~/.acp/hooks/grok-build/hook.mjs # expected sha256: $ACP_PIN_GROK_HOOK_SHA"
echo " curl -fsSL $_grok_raw/hooks/acp.json -o ~/.grok/hooks/acp.json # expected sha256: $ACP_PIN_GROK_JSON_SHA"
echo " Guide: https://agenticcontrolplane.com/integrations/grok-build"
fi
echo ""
fi
# Google Antigravity (agy) — CLI, IDE, and app read ONE shared registration at
# ~/.gemini/config/hooks.json. We MERGE under our own "acp" key via node (the
# hook needs node anyway) — never overwrite: the file may hold user hooks.
if [ "$HAS_AGY" = true ] && [ "$LOCAL_MODE" = true ]; then
echo " ${C_DIM}Antigravity detected — skipped in --local mode (its ACP hook needs a workspace; local decisions aren't wired there yet).${C_RESET}"
echo ""
fi
if [ "$HAS_AGY" = true ] && [ "$LOCAL_MODE" = false ]; then
echo " Detected Google Antigravity — installing the ACP hook…"
AGY_HOOK_OK=false
_agy_raw="https://raw.githubusercontent.com/agentic-control-plane/antigravity-acp-plugin/$ACP_PIN_AGY_REF"
if command -v node > /dev/null 2>&1 \
&& mkdir -p "$HOME/.acp/hooks/antigravity" "$HOME/.gemini/config" 2>/dev/null \
&& _acp_fetch_pinned "$_agy_raw/hook.mjs" "$ACP_PIN_AGY_HOOK_SHA" "$HOME/.acp/hooks/antigravity/hook.mjs" \
&& _acp_fetch_pinned "$_agy_raw/hooks/acp.json" "$ACP_PIN_AGY_JSON_SHA" "$HOME/.acp/hooks/antigravity/acp.json" \
&& node -e 'const fs=require("fs");const p=process.env.HOME+"/.gemini/config/hooks.json";let cur={};try{cur=JSON.parse(fs.readFileSync(p,"utf8"))}catch(e){};const add=JSON.parse(fs.readFileSync(process.env.HOME+"/.acp/hooks/antigravity/acp.json","utf8"));fs.writeFileSync(p,JSON.stringify(Object.assign(cur,add),null,2))' 2>/dev/null; then
AGY_HOOK_OK=true
else
_acp_pin_warn "an Antigravity hook file" "Hook not registered."
fi
if [ "$AGY_HOOK_OK" = true ]; then
echo " ${C_GREEN}✓ Antigravity governed${C_RESET} — registration merged into the shared hooks.json (CLI, IDE, and app); ACP asks render as native force_ask prompt cards; active from the next session."
INSTALLED="${INSTALLED:+$INSTALLED, }Antigravity"
else
echo " Couldn't finish automatically (node/curl refused or a directory wasn't writable). Run:"
echo " mkdir -p ~/.acp/hooks/antigravity ~/.gemini/config"
echo " curl -fsSL $_agy_raw/hook.mjs -o ~/.acp/hooks/antigravity/hook.mjs # expected sha256: $ACP_PIN_AGY_HOOK_SHA"
echo " curl -fsSL $_agy_raw/hooks/acp.json -o ~/.acp/hooks/antigravity/acp.json # expected sha256: $ACP_PIN_AGY_JSON_SHA"
echo " node -e 'see https://agenticcontrolplane.com/integrations/antigravity#manual-install'"
echo " Guide: https://agenticcontrolplane.com/integrations/antigravity"
fi
echo ""
fi
# Plugin-governed harnesses (Hermes, dsh, pi, Prime, Muse, Grok, Antigravity)
# are wired above, but they still need what comes AFTER this point: Step 1f
# writes their priced launcher, provider block and agent directive, and
# Step 2 puts the key on this machine (their plugins read ~/.acp/credentials).
# An earlier version exited 0 here on a "hermes-only / pi-only box" — that
# skipped all of it, and because bash was still streaming the script from
# curl, the early exit broke the pipe and `curl | bash` reported 23 even
# though the plugin had installed (harness canary, 2026-10-07). So: only the
# truly-nothing-found case stops here; everything else falls through.
if [ "$HAS_CLAUDE" = false ] && [ "$HAS_CURSOR" = false ] && [ "$HAS_CODEX" = false ] && [ "$HAS_OPENCLAW" = false ] && [ "$HAS_OPENCODE" = false ] && [ "$HAS_QWEN" = false ] && [ "$HAS_COPILOT" = false ] \
&& [ "$HAS_HERMES" = false ] && [ "$HAS_DSH" = false ] && [ "$HAS_PI" = false ] && [ "$HAS_PRIME" = false ] && [ "$HAS_MUSE" = false ] && [ "$HAS_GROK" = false ] && [ "$HAS_AGY" = false ]; then
echo " ${C_RED}No supported AI clients detected.${C_RESET}"
echo " Supported: Claude Code, Cursor, OpenAI Codex CLI, GitHub Copilot, OpenClaw, opencode, Qwen Code, pi, Prime Agent, Muse Code, Grok Build, Antigravity, Hermes Agent, DeepSeek Harness"
echo " Hermes Agent? It has a native pip plugin instead:"
echo " pip install acp-hermes && hermes plugins enable acp && acp-hermes login"
echo " Guide: https://agenticcontrolplane.com/integrations/hermes"
echo " DeepSeek Harness? Same idea, its native plugin system:"
echo " dsh plugin --profile <your-profile> add @agenticcontrolplane/dsh"
echo " Guide: https://github.com/agentic-control-plane/dsh-acp-plugin"
echo " pi (earendil-works)? A native extension file:"
echo " mkdir -p ~/.pi/agent/extensions && curl -sf https://raw.githubusercontent.com/agentic-control-plane/pi-acp-plugin/$ACP_PIN_PI_REF/index.ts -o ~/.pi/agent/extensions/acp.ts # expected sha256: $ACP_PIN_PI_INDEX_SHA"
echo " Guide: https://agenticcontrolplane.com/integrations/pi"
echo " Prime Agent? Same idea, a native extension file:"
echo " mkdir -p ~/.prime/agent/extensions && curl -sf https://raw.githubusercontent.com/agentic-control-plane/prime-agent-acp-plugin/$ACP_PIN_PRIME_REF/index.ts -o ~/.prime/agent/extensions/acp.ts # expected sha256: $ACP_PIN_PRIME_INDEX_SHA"
echo " Guide: https://agenticcontrolplane.com/integrations/prime-agent"
echo ""
echo " Install one first, then re-run this script."
exit 1
fi
TARGETS=""
if [ "$HAS_CLAUDE" = true ]; then TARGETS="Claude Code"; fi
if [ "$HAS_CURSOR" = true ]; then
if [ -n "$TARGETS" ]; then TARGETS="$TARGETS + Cursor"; else TARGETS="Cursor"; fi
fi
if [ "$HAS_CODEX" = true ]; then
if [ -n "$TARGETS" ]; then TARGETS="$TARGETS + Codex"; else TARGETS="Codex"; fi
fi
if [ "$HAS_OPENCLAW" = true ]; then
if [ -n "$TARGETS" ]; then TARGETS="$TARGETS + OpenClaw"; else TARGETS="OpenClaw"; fi
fi
if [ "$HAS_OPENCODE" = true ]; then
if [ -n "$TARGETS" ]; then TARGETS="$TARGETS + opencode"; else TARGETS="opencode"; fi
fi
if [ "$HAS_QWEN" = true ]; then
if [ -n "$TARGETS" ]; then TARGETS="$TARGETS + Qwen Code"; else TARGETS="Qwen Code"; fi
fi
if [ "$HAS_COPILOT" = true ]; then
if [ -n "$TARGETS" ]; then TARGETS="$TARGETS + GitHub Copilot"; else TARGETS="GitHub Copilot"; fi
fi
[ "$HAS_HERMES" = true ] && TARGETS="${TARGETS:+$TARGETS + }Hermes"
[ "$HAS_DSH" = true ] && TARGETS="${TARGETS:+$TARGETS + }DeepSeek Harness"
[ "$HAS_PI" = true ] && TARGETS="${TARGETS:+$TARGETS + }pi"
[ "$HAS_PRIME" = true ] && TARGETS="${TARGETS:+$TARGETS + }Prime Agent"
[ "$HAS_MUSE" = true ] && TARGETS="${TARGETS:+$TARGETS + }Muse Code"
[ "$HAS_GROK" = true ] && TARGETS="${TARGETS:+$TARGETS + }Grok Build"
[ "$HAS_AGY" = true ] && TARGETS="${TARGETS:+$TARGETS + }Antigravity"
# Machine-readable form of the same detection, sent with the device-code
# request so the minted key records WHICH harness it was wired for. The
# gateway defaults this to "unknown" when absent, which is what every key
# minted through device flow has been labelled until now — and device flow
# is the best-converting install path we have, so it was also the only one
# we could not attribute. Gateway clips to 64 chars.
CLIENT_SLUG=""
_add_slug() { if [ -n "$CLIENT_SLUG" ]; then CLIENT_SLUG="$CLIENT_SLUG+$1"; else CLIENT_SLUG="$1"; fi; }
[ "$HAS_CLAUDE" = true ] && _add_slug "claude-code"
[ "$HAS_CURSOR" = true ] && _add_slug "cursor"
[ "$HAS_CODEX" = true ] && _add_slug "codex"
[ "$HAS_OPENCLAW" = true ] && _add_slug "openclaw"
[ "$HAS_OPENCODE" = true ] && _add_slug "opencode"
[ "$HAS_QWEN" = true ] && _add_slug "qwen-code"
[ "$HAS_COPILOT" = true ] && _add_slug "copilot"
[ "$HAS_HERMES" = true ] && _add_slug "hermes"
[ "$HAS_DSH" = true ] && _add_slug "dsh"
[ "$HAS_PI" = true ] && _add_slug "pi"
[ "$HAS_PRIME" = true ] && _add_slug "prime-agent"
[ "$HAS_MUSE" = true ] && _add_slug "muse"
[ "$HAS_GROK" = true ] && _add_slug "grok"
[ "$HAS_AGY" = true ] && _add_slug "antigravity"
[ -n "$CLIENT_SLUG" ] || CLIENT_SLUG="cli"
echo ""
echo " Wiring $TARGETS"
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
mkdir -p "$CONFIG_DIR"
# ── Shared: write govern.mjs ──────────────────────────────────────────
# Single dispatcher script used by non-plugin clients (Cursor, Codex,
# opencode) and as the Claude Code fallback. The CANONICAL copy lives in
# the plugin repo (bin/govern.mjs — scoped-token exchange + the govern.*
# control-plane endpoint); fetch it so hook fixes reach these harnesses
# without an installer release. The inline copy below is the offline
# fallback only.
# Old version, read from whatever is already on disk, before it's
# overwritten below — acp-update prints old -> new so a refresh is visible.
GOVERN_OLD_VERSION="$(grep -o 'PLUGIN_VERSION = "[^"]*"' "$CONFIG_DIR/govern.mjs" 2>/dev/null | head -1 | sed 's/.*"\(.*\)"/\1/')"
echo " [ACP] Installing governance hook script..."
GOVERN_RAW_URL="https://raw.githubusercontent.com/agentic-control-plane/$ACP_PIN_GOVERN_REPO/$ACP_PIN_GOVERN_REF/bin/govern.mjs"
# In --local mode the fetched copy must actually carry the local decision
# path (runLocal). A canonical copy without it would exit silently on every
# call — "local mode active" with zero governance and zero audit. Never
# brick, never silently: such a copy is rejected in favor of the bundled one.
# Pinned (acp-install#29): fetched to a private temp file, sha256-verified
# against ACP_PIN_GOVERN_SHA, then renamed into place. Offline, a 404, or a
# checksum mismatch all land on the bundled copy below — never silently: a
# rejected file warns, an unreachable one prints a one-line notice, and a
# verified file that fails the sanity checks says which check. The install
# never prompts and never fails here.
GOVERN_FETCHED="$CONFIG_DIR/govern.mjs.pinned"
rm -f "$GOVERN_FETCHED"
GOVERN_PINNED_OK=false
if _acp_fetch_pinned "$GOVERN_RAW_URL" "$ACP_PIN_GOVERN_SHA" "$GOVERN_FETCHED"; then
if ! head -1 "$GOVERN_FETCHED" | grep -q "node"; then
echo " ${C_RED}[ACP] WARNING:${C_RESET} pinned govern.mjs verified but has no node shebang — using the bundled copy." >&2
elif [ "$LOCAL_MODE" = true ] && ! grep -q "runLocal" "$GOVERN_FETCHED"; then
echo " ${C_RED}[ACP] WARNING:${C_RESET} pinned govern.mjs verified but lacks the local decision path (runLocal) — using the bundled copy so --local keeps governing." >&2
elif mv -f "$GOVERN_FETCHED" "$CONFIG_DIR/govern.mjs"; then
GOVERN_PINNED_OK=true
else
echo " ${C_RED}[ACP] WARNING:${C_RESET} could not move the verified govern.mjs into place — using the bundled copy." >&2
fi
elif [ "$ACP_PIN_FAIL" = "checksum" ]; then
_acp_pin_warn "govern.mjs" "Using the bundled copy instead."
else
echo " ${C_DIM}[ACP] pinned govern.mjs unavailable, using bundled copy${C_RESET}" >&2
fi
if [ "$GOVERN_PINNED_OK" = true ]; then
GOVERN_SOURCE="canonical (plugin repo, pinned + sha256-verified)"
else
rm -f "$GOVERN_FETCHED"
GOVERN_SOURCE="bundled fallback"
cat > "$CONFIG_DIR/govern.mjs" << 'GOVERN'
#!/usr/bin/env node
import { readFileSync, existsSync, appendFileSync, mkdirSync, writeFileSync, statSync, openSync, readSync, closeSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const ACP_API = process.env.ACP_API_BASE || "https://api.agenticcontrolplane.com";
const PLUGIN_VERSION = "0.5.0";
// Identifies the calling client to the server (per-client policy routing).
// Each client's hooks.json sets this env var at invocation time: "claude-code-plugin",
// "cursor", "codex", etc. Falls back to claude-code-plugin for backward compat.
const ACP_CLIENT = process.env.ACP_CLIENT || "claude-code-plugin";
const POST_HOOK_PAYLOAD_CEILING = 200 * 1024;
function readToken() {
if (process.env.ACP_BEARER_TOKEN) return process.env.ACP_BEARER_TOKEN;
// Both credential paths, in the same order as the plugin's
// bin/mcp-auth-headers.sh — these MUST stay in sync. When only proxy-key
// exists, MCP authenticates (the install looks healthy) while a
// credentials-only hook reads nothing and no-ops (#596).
for (const name of ["credentials", "proxy-key"]) {
try {
const value = readFileSync(join(homedir(), ".acp", name), "utf8").trim();
if (value) return value;
} catch { /* absent or unreadable — try the next path */ }
}
return null;
}
const ACP_DIR = join(homedir(), ".acp");
const token = readToken();
// LOCAL mode: no account, no cloud. Active when there's no token AND either a
// local policy exists or ACP_LOCAL=1. Decisions are made on-device by
// decide.mjs against ~/.acp/policy.json; calls are logged to ~/.acp/audit.jsonl.
const LOCAL = !token && (process.env.ACP_LOCAL === "1" || existsSync(join(ACP_DIR, "policy.json")));
// Read stdin via async iteration, not readFileSync("/dev/stdin"): on Linux a
// non-blocking pipe makes the sync read throw EAGAIN, which would silently
// skip governance for every call. (Caught by CI on ubuntu.) Read before the
// no-credentials check so the warning can name the session and the tool.
let input;
try {
process.stdin.setEncoding("utf8");
let raw = "";
for await (const chunk of process.stdin) raw += chunk;
input = JSON.parse(raw);
} catch { process.exit(0); }
// Wired but uncredentialed: never brick — but NEVER silently (#592). The
// canonical bin/govern.mjs warns loudly here; this offline fallback must
// match, or the no-network install path keeps the exact silent-ungoverned
// hole the front-door fix closed. Banner once per session (marker-deduped);
// a lapse.log line every call — the durable, auditable record of the gap.
if (!token && !LOCAL) {
const sessionId = String(input?.session_id ?? "unknown");
try { mkdirSync(ACP_DIR, { recursive: true }); } catch { /* best-effort */ }
try {
appendFileSync(join(ACP_DIR, "lapse.log"),
`${new Date().toISOString()}\tUNGOVERNED\tno-credentials\tclient=${ACP_CLIENT}\tsession=${sessionId}\ttool=${input?.tool_name ?? "?"}\n`);
} catch { /* the lapse log is best-effort — never block a call on it */ }
let seen = false;
try { seen = readFileSync(join(ACP_DIR, "nocred-session"), "utf8").trim() === sessionId; } catch {}
if (!seen) {
try { writeFileSync(join(ACP_DIR, "nocred-session"), sessionId); } catch { /* at worst the banner repeats */ }
process.stdout.write(JSON.stringify({
systemMessage: