@linzumi/cli 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +305 -251
- package/package.json +1 -1
- package/scripts/qa/codex-keychain-partition-repro.md +128 -0
- package/scripts/qa/codex-keychain-partition-repro.sh +151 -0
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Codex MCP keychain re-prompt - fix + QA
|
|
2
|
+
|
|
3
|
+
Spec: `kandan/server_v2/plans/2026-06-24-codex-mcp-keychain-reprompt-report.md`
|
|
4
|
+
|
|
5
|
+
## The problem (one paragraph)
|
|
6
|
+
|
|
7
|
+
Codex stores its MCP OAuth secret in a macOS login-keychain generic-password
|
|
8
|
+
item labeled `Codex MCP Credentials`. The item's access-control list is bound to
|
|
9
|
+
one exact codex binary (its CDHash). When Codex.app auto-updates (new CDHash),
|
|
10
|
+
or you move between codex identities (official OpenAI team `2DC432GLL2`, the
|
|
11
|
+
unsigned npm `codex` under `node`, or a Linzumi-re-signed codex), the CDHash no
|
|
12
|
+
longer matches the ACL and macOS re-prompts: "codex wants to use the Codex MCP
|
|
13
|
+
Credentials in your keychain". The proper upstream fixes (a Designated-
|
|
14
|
+
Requirement-bound ACL, or moving to the data-protection keychain) are OpenAI's.
|
|
15
|
+
|
|
16
|
+
## The no-upstream fix (lever 2)
|
|
17
|
+
|
|
18
|
+
Set the keychain item's PARTITION LIST to the code-signing team id(s) so any
|
|
19
|
+
same-team-signed codex build is granted silent access across updates, instead of
|
|
20
|
+
binding access to a single CDHash. This is what the
|
|
21
|
+
`linzumi fix-codex-keychain` subcommand does. The exact command it runs per
|
|
22
|
+
matching item is:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
security set-generic-password-partition-list \
|
|
26
|
+
-S "teamid:2DC432GLL2,teamid:2CWCP5BR45,apple:" \
|
|
27
|
+
-s "Codex MCP Credentials" \
|
|
28
|
+
-a <account> \
|
|
29
|
+
-k <login-keychain-password>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `2DC432GLL2` - OpenAI Codex code-signing team (the official Codex.app).
|
|
33
|
+
- `2CWCP5BR45` - Linzumi's macOS team
|
|
34
|
+
(`kandan/kandan-electron-shell/scripts/common_macos_beta.sh` `DEFAULT_TEAM_ID`,
|
|
35
|
+
overridable via `KANDAN_DEVELOPMENT_TEAM`; add more with `--team-id`).
|
|
36
|
+
- `apple:` - keeps Apple-provided tooling working.
|
|
37
|
+
|
|
38
|
+
The helper is macOS-only, scoped exclusively to the `Codex MCP Credentials`
|
|
39
|
+
service, idempotent, and consent-gated: it defaults to a dry run and only
|
|
40
|
+
mutates with `--apply`. The login-keychain password is never logged or
|
|
41
|
+
persisted (pass it via `LINZUMI_KEYCHAIN_PASSWORD` for automation, or let macOS
|
|
42
|
+
prompt).
|
|
43
|
+
|
|
44
|
+
### Using the helper
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
# Dry run - shows the item(s) and the partition list it would apply.
|
|
48
|
+
linzumi fix-codex-keychain
|
|
49
|
+
|
|
50
|
+
# Apply. macOS authorizes the ACL change with your login-keychain password.
|
|
51
|
+
linzumi fix-codex-keychain --apply
|
|
52
|
+
|
|
53
|
+
# Add an extra signing team (e.g. a custom re-signed codex).
|
|
54
|
+
linzumi fix-codex-keychain --apply --team-id ABCDE12345
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Automated synthetic repro (no GUI, no real item)
|
|
58
|
+
|
|
59
|
+
`scripts/qa/codex-keychain-partition-repro.sh` proves the mechanism the helper
|
|
60
|
+
relies on, without touching your real login keychain or the real
|
|
61
|
+
`Codex MCP Credentials` item, and without needing a real Codex.app update.
|
|
62
|
+
|
|
63
|
+
It creates a dedicated, unlocked test keychain with a known password, adds a
|
|
64
|
+
test item whose partition list does NOT include the target team (the would-
|
|
65
|
+
re-prompt precondition), runs the exact `set-generic-password-partition-list`
|
|
66
|
+
command the helper runs, then reads the partition list back with
|
|
67
|
+
`security dump-keychain -a` to prove the target team flipped from absent to
|
|
68
|
+
present. The test keychain is deleted on exit via a `trap` (even on failure).
|
|
69
|
+
|
|
70
|
+
Run it:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
bash packages/linzumi-cli/scripts/qa/codex-keychain-partition-repro.sh
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Expected output ends with:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
== BEFORE: target team must be ABSENT from the partition list ==
|
|
80
|
+
partition list (before): apple:
|
|
81
|
+
ok: target team is absent before the fix (a same-team codex would re-prompt).
|
|
82
|
+
...
|
|
83
|
+
== AFTER: target team must now be PRESENT in the partition list ==
|
|
84
|
+
partition list (after): teamid:2DC432GLL2, apple:
|
|
85
|
+
ok: target team is present after the fix (same-team codex builds read silently).
|
|
86
|
+
...
|
|
87
|
+
RESULT: PASS (partition list flipped from team-absent to team-present)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### What the synthetic repro does and does NOT prove
|
|
91
|
+
|
|
92
|
+
- PROVES (headless, deterministic): the fix command writes the intended
|
|
93
|
+
partition list onto the item; the team id transitions from absent to present,
|
|
94
|
+
observed verbatim via `security dump-keychain -a`. This is the load-bearing
|
|
95
|
+
state change.
|
|
96
|
+
- Does NOT prove headlessly: that a same-team-signed codex binary then reads the
|
|
97
|
+
secret with zero GUI prompt. Demonstrating the silent read requires a binary
|
|
98
|
+
actually signed by the team in the partition; `/usr/bin/security` is not, and
|
|
99
|
+
the authorization for a non-member reader is a GUI event that cannot be
|
|
100
|
+
satisfied non-interactively (it returns errSec interaction-not-allowed in an
|
|
101
|
+
automated shell). That payoff is confirmed by the on-device manual procedure
|
|
102
|
+
below, which is itself inherently a GUI observation (the re-prompt is a dialog).
|
|
103
|
+
|
|
104
|
+
## On-device manual verification (real Codex.app item)
|
|
105
|
+
|
|
106
|
+
This is the human, on-device confirmation of the end-user payoff. Do it on a Mac
|
|
107
|
+
that actually uses Codex.app MCP credentials.
|
|
108
|
+
|
|
109
|
+
1. Observe the BEFORE re-prompt. Trigger codex MCP usage (or force a re-prompt by
|
|
110
|
+
updating Codex.app, or temporarily switching codex identities). macOS shows:
|
|
111
|
+
"codex wants to use the Codex MCP Credentials in your keychain". This dialog
|
|
112
|
+
is the bug; note that it appears.
|
|
113
|
+
2. Apply the fix:
|
|
114
|
+
```
|
|
115
|
+
linzumi fix-codex-keychain # dry run: confirm the item + plan
|
|
116
|
+
linzumi fix-codex-keychain --apply # enter your login-keychain password once
|
|
117
|
+
```
|
|
118
|
+
3. Confirm AFTER. Relaunch codex / Codex.app (or re-trigger the same MCP usage,
|
|
119
|
+
ideally after an app update so the CDHash differs). The credential is read
|
|
120
|
+
with NO re-prompt. To double-check the state without a relaunch:
|
|
121
|
+
```
|
|
122
|
+
security dump-keychain -a login.keychain-db \
|
|
123
|
+
| grep -A2 'authorizations (1): partition_id'
|
|
124
|
+
```
|
|
125
|
+
The `description:` line for the `Codex MCP Credentials` item now lists
|
|
126
|
+
`teamid:2DC432GLL2` (and `teamid:2CWCP5BR45`, `apple:`).
|
|
127
|
+
|
|
128
|
+
Re-running `linzumi fix-codex-keychain --apply` is harmless (idempotent).
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Spec: plans/2026-06-24-codex-mcp-keychain-reprompt-report.md
|
|
4
|
+
#
|
|
5
|
+
# Self-contained, NON-INTERACTIVE proof of the Codex MCP keychain partition-list
|
|
6
|
+
# fix (lever 2). It NEVER touches your real login keychain or the real
|
|
7
|
+
# "Codex MCP Credentials" item, and it does NOT require a real Codex.app update.
|
|
8
|
+
#
|
|
9
|
+
# WHAT IT PROVES (headless, deterministic):
|
|
10
|
+
# The fix runs `security set-generic-password-partition-list` to add the
|
|
11
|
+
# code-signing team(s) to a keychain item's PARTITION LIST. A keychain item's
|
|
12
|
+
# silent-read access is gated by that partition list. We create a dedicated
|
|
13
|
+
# test item whose partition list does NOT include the target team
|
|
14
|
+
# ("teamid:2DC432GLL2"), confirm the team is ABSENT (the "broken" / would-
|
|
15
|
+
# re-prompt precondition), run the SAME command the helper runs, and confirm
|
|
16
|
+
# the team is now PRESENT (the "fixed" state). The partition list is read back
|
|
17
|
+
# verbatim via `security dump-keychain -a`, so the before/after transition is
|
|
18
|
+
# fully observable without any GUI.
|
|
19
|
+
#
|
|
20
|
+
# WHY NOT "silent read" headlessly:
|
|
21
|
+
# The post-fix payoff is that a same-team-signed codex binary reads the secret
|
|
22
|
+
# with NO GUI prompt. Demonstrating that requires a binary that is actually in
|
|
23
|
+
# the team partition; /usr/bin/security is not, and the authorization is a GUI
|
|
24
|
+
# event. That on-device confirmation is the manual procedure in the QA doc.
|
|
25
|
+
# This script proves the load-bearing state change the helper performs.
|
|
26
|
+
#
|
|
27
|
+
# Everything happens inside a DEDICATED test keychain with a KNOWN password,
|
|
28
|
+
# created unlocked and deleted on exit (even on failure, via trap), so macOS
|
|
29
|
+
# never pops a GUI dialog and your real keychains are untouched.
|
|
30
|
+
#
|
|
31
|
+
# Usage: bash codex-keychain-partition-repro.sh
|
|
32
|
+
# Exit: 0 = PASS, 1 = FAIL, 2 = not macOS.
|
|
33
|
+
|
|
34
|
+
set -u
|
|
35
|
+
|
|
36
|
+
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
37
|
+
echo "SKIP: this repro only runs on macOS (darwin). Detected: $(uname -s)" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
KEYCHAIN_NAME="linzumi-qa.keychain"
|
|
42
|
+
KEYCHAIN_PW="linzumi-qa-$$"
|
|
43
|
+
SERVICE="Codex MCP Credentials TEST"
|
|
44
|
+
ACCOUNT="testacct"
|
|
45
|
+
SECRET="testsecret-$$"
|
|
46
|
+
# The official OpenAI Codex team id is the partition we add (the same default
|
|
47
|
+
# the helper applies). The synthetic item proves the partition-list mechanism;
|
|
48
|
+
# the team id value is not verified against a real signature in this test.
|
|
49
|
+
TARGET_TEAM="2DC432GLL2"
|
|
50
|
+
# BEFORE: a partition list WITHOUT the target team. AFTER: target team + apple.
|
|
51
|
+
BEFORE_PARTITION="apple:"
|
|
52
|
+
AFTER_PARTITION="teamid:${TARGET_TEAM},apple:"
|
|
53
|
+
|
|
54
|
+
cleanup() {
|
|
55
|
+
security delete-keychain "$KEYCHAIN_NAME" >/dev/null 2>&1 || true
|
|
56
|
+
}
|
|
57
|
+
trap cleanup EXIT
|
|
58
|
+
|
|
59
|
+
# Read the partition-list "description:" line (under the partition_id
|
|
60
|
+
# authorization) for the test item from a keychain dump.
|
|
61
|
+
read_partition_description() {
|
|
62
|
+
security dump-keychain -a "$KEYCHAIN_NAME" 2>/dev/null \
|
|
63
|
+
| grep -A2 'authorizations (1): partition_id' \
|
|
64
|
+
| grep 'description:' \
|
|
65
|
+
| head -n1 \
|
|
66
|
+
| sed 's/^[[:space:]]*description:[[:space:]]*//'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
echo "== Codex MCP keychain partition-list repro (synthetic, non-interactive) =="
|
|
70
|
+
echo "test keychain: $KEYCHAIN_NAME"
|
|
71
|
+
echo "service: $SERVICE"
|
|
72
|
+
echo "target team: teamid:$TARGET_TEAM"
|
|
73
|
+
echo
|
|
74
|
+
|
|
75
|
+
# Clean slate in case a prior aborted run left the keychain behind.
|
|
76
|
+
cleanup
|
|
77
|
+
|
|
78
|
+
echo "[setup] create + unlock dedicated test keychain"
|
|
79
|
+
if ! security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN_NAME"; then
|
|
80
|
+
echo "FAIL: could not create test keychain" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
# Disable auto-relock so no GUI unlock prompt can appear during the run.
|
|
84
|
+
security set-keychain-settings "$KEYCHAIN_NAME" || true
|
|
85
|
+
security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN_NAME" || true
|
|
86
|
+
|
|
87
|
+
echo "[setup] add a test item (permissive ACL) then set a partition list that"
|
|
88
|
+
echo " EXCLUDES the target team (the would-re-prompt precondition)"
|
|
89
|
+
if ! security add-generic-password \
|
|
90
|
+
-a "$ACCOUNT" -s "$SERVICE" -w "$SECRET" -A "$KEYCHAIN_NAME"; then
|
|
91
|
+
echo "FAIL: could not add test item" >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN_NAME" || true
|
|
95
|
+
if ! security set-generic-password-partition-list \
|
|
96
|
+
-S "$BEFORE_PARTITION" \
|
|
97
|
+
-s "$SERVICE" -a "$ACCOUNT" -k "$KEYCHAIN_PW" "$KEYCHAIN_NAME" \
|
|
98
|
+
>/dev/null 2>&1; then
|
|
99
|
+
echo "FAIL: could not set the BEFORE partition list" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
echo
|
|
104
|
+
echo "== BEFORE: target team must be ABSENT from the partition list =="
|
|
105
|
+
BEFORE_DESC="$(read_partition_description)"
|
|
106
|
+
echo "partition list (before): ${BEFORE_DESC:-<none>}"
|
|
107
|
+
if echo "$BEFORE_DESC" | grep -q "teamid:${TARGET_TEAM}"; then
|
|
108
|
+
echo "BEFORE precondition NOT met: target team already present." >&2
|
|
109
|
+
BEFORE_OK=0
|
|
110
|
+
else
|
|
111
|
+
echo "ok: target team is absent before the fix (a same-team codex would re-prompt)."
|
|
112
|
+
BEFORE_OK=1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
echo
|
|
116
|
+
echo "== APPLY FIX: the exact command the helper runs =="
|
|
117
|
+
echo "running: security set-generic-password-partition-list -S \"$AFTER_PARTITION\" -s \"$SERVICE\" -a \"$ACCOUNT\" -k <pw> $KEYCHAIN_NAME"
|
|
118
|
+
security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN_NAME" || true
|
|
119
|
+
if ! security set-generic-password-partition-list \
|
|
120
|
+
-S "$AFTER_PARTITION" \
|
|
121
|
+
-s "$SERVICE" -a "$ACCOUNT" -k "$KEYCHAIN_PW" "$KEYCHAIN_NAME" \
|
|
122
|
+
>/dev/null 2>&1; then
|
|
123
|
+
echo "FAIL: set-generic-password-partition-list failed" >&2
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
echo "partition list applied."
|
|
127
|
+
|
|
128
|
+
echo
|
|
129
|
+
echo "== AFTER: target team must now be PRESENT in the partition list =="
|
|
130
|
+
AFTER_DESC="$(read_partition_description)"
|
|
131
|
+
echo "partition list (after): ${AFTER_DESC:-<none>}"
|
|
132
|
+
if echo "$AFTER_DESC" | grep -q "teamid:${TARGET_TEAM}"; then
|
|
133
|
+
echo "ok: target team is present after the fix (same-team codex builds read silently)."
|
|
134
|
+
AFTER_OK=1
|
|
135
|
+
else
|
|
136
|
+
echo "AFTER FAILED: target team is still absent." >&2
|
|
137
|
+
AFTER_OK=0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo
|
|
141
|
+
echo "============================== VERDICT =============================="
|
|
142
|
+
echo "BEFORE (team absent): $BEFORE_OK"
|
|
143
|
+
echo "AFTER (team present): $AFTER_OK"
|
|
144
|
+
|
|
145
|
+
if [[ $BEFORE_OK -eq 1 && $AFTER_OK -eq 1 ]]; then
|
|
146
|
+
echo "RESULT: PASS (partition list flipped from team-absent to team-present)"
|
|
147
|
+
exit 0
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
echo "RESULT: FAIL"
|
|
151
|
+
exit 1
|