@mmerterden/multi-agent-pipeline 20.9.0 → 20.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/docs/facts.json +5 -4
- package/install/catalog-history.json +1 -1
- package/manifest.json +31 -28
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +12 -13
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +63 -19
- package/pipeline/lib/credential-inventory.sh +3 -2
- package/pipeline/lib/repo-profile-derive.mjs +21 -12
- package/pipeline/multi-agent-refs/features/doctor.md +17 -0
- package/pipeline/multi-agent-refs/features/repo-profile.md +1 -1
- package/pipeline/multi-agent-refs/keychain.md +2 -0
- package/pipeline/multi-agent-refs/outside-the-pipeline.md +1 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +106 -20
- package/pipeline/multi-agent-refs/setup/google-play.md +70 -0
- package/pipeline/preferences-template.json +3 -1
- package/pipeline/schemas/prefs.schema.json +9 -1
- package/pipeline/schemas/repo-profile.schema.json +32 -1
- package/pipeline/scripts/doctor.mjs +91 -1
- package/pipeline/scripts/pre-commit-check.sh +11 -2
- package/pipeline/scripts/probe-store-key.mjs +306 -0
- package/pipeline/scripts/write-store-config.mjs +293 -0
- package/pipeline/skills/.skill-manifest.json +4 -4
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +58 -6
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +11 -7
|
@@ -19,7 +19,7 @@ Every role is `{ value, source, evidence, confidence }`. `source` is `derived`,
|
|
|
19
19
|
| `commit.format` | last 200 non-merge, non-bot subjects: ticket `prefix` / `suffix` / `none` by majority, its style (`[KEY-123]`, `KEY-123:`), whether a conventional scope is required. A trailing `(#123)` is the host's PR number and is ignored |
|
|
20
20
|
| `repo.workBranch`, `repo.defaultBranch` | `pull_request: branches:` targets in workflows, else `push: branches:` targets, else the most active of the usual integration branches, remote or local; `origin/HEAD` |
|
|
21
21
|
| `build`, `test`, `lint` | CI `run:` steps, exact flags kept, continuations joined; `high` when a pull request triggers the step or it appears twice; `-testPlan` values and `*.xctestplan` files; `package.json` scripts as a low-confidence fallback |
|
|
22
|
-
| `accessors.*`, `di.*` | top-level types a generated localization / testing-id / token tree declares, ranked by `<Root>.<Seg>.<Seg>` usage outside it; the registrar suffix is the compound most files of the commonest `Registrar` / `Configurator` / `Assembly` / `Container` / `Module` family end in, read from the file names; its
|
|
22
|
+
| `accessors.*`, `di.*` | top-level types a generated localization / testing-id / token tree declares, ranked by `<Root>.<Seg>.<Seg>` usage outside it; the registrar suffix is the compound most files of the commonest `Registrar` / `Configurator` / `Assembly` / `Container` / `Module` family end in, read from the file names; its commonest registration method (`registerMethod`) and, apart from it, its commonest `validate*` / `verify*` / `check*` method (`validateMethod`); the inject attribute (`@Inject`, `@Injected`, `@Dependency` and similar keyed wrappers). `--conventions <file>` or `--with-conventions <platform>` folds in `extract-conventions.sh` buckets |
|
|
23
23
|
| `module`, `mock` | repeated module manifests as a glob; a module validator script or step; mock framework imports or a fixture tree; a `Custom*` tree beside a generated one. `mock.system` and `di.injectAttribute` count only files of the repo's dominant language and carry `file:line` evidence; otherwise they are `null` |
|
|
24
24
|
| `hooks[]` | `.githooks/`, `scripts/git-hooks/`, `.husky/`, pre-commit and lefthook configs; side effects `amends-commit`, `stages-files`, `writes-files`, `network`; the script that sets `core.hooksPath` |
|
|
25
25
|
| `requiredChecks[]`, `docs.authoritative[]`, `inRepoSkills[]` | workflow jobs with a `required` comment (always low); the documents `claude-md-links.mjs` admits from the repo's `CLAUDE.md` files (same bounds, evidence is the CLAUDE.md that points at it); `.claude/skills/*/SKILL.md` with `trigger: explicit` when `disable-model-invocation: true` |
|
|
@@ -163,6 +163,8 @@ The shell driver auto-delegates to `~/.claude/scripts/keychain.py` for determini
|
|
|
163
163
|
| `vercel` | Vercel deploys | `${USER}_Vercel_Access_Token` | Access Token | Vercel -> Account Settings -> Tokens. Read by `vercel-deploy.sh`; never passed on argv |
|
|
164
164
|
| `appstore_connect_key_id` | App Store Connect | `${USER}_AppStoreConnect_Key_Id` | Identifier, not a secret | App Store Connect -> Users and Access -> Integrations -> App Store Connect API |
|
|
165
165
|
| `appstore_connect_issuer_id` | App Store Connect | `${USER}_AppStoreConnect_Issuer_Id` | Identifier, not a secret | Same page as the key id; one issuer id per team |
|
|
166
|
+
| `appstore_connect_private_key` | App Store Connect | `${USER}_AppStoreConnect_Private_Key` | Full PEM text of `AuthKey_<keyId>.p8` | Same page as the key id; downloadable once. Stored from the file via stdin (`set appstore_connect_private_key - < AuthKey_<keyId>.p8`), probed first with `probe-store-key.mjs --asc` |
|
|
167
|
+
| `google_play` | Google Play | `${USER}_GooglePlay_Access_Json` | Service-account JSON, as issued | Play Console -> Setup -> API access; invite the account with app permissions and enable the Google Play Android Developer API and the Google Play Developer Reporting API. Probed first with `probe-store-key.mjs --play` |
|
|
166
168
|
| `appstore_connect_apple_id` | App Store Connect | `${USER}_AppStoreConnect_Apple_Id` | Email address | The Apple ID itself; no generation step |
|
|
167
169
|
| `appstore_connect_password_item` | App Store Connect | `${USER}_AppStoreConnect_Password_Item` | Keychain ITEM NAME, not a password | `xcrun altool --store-password-in-keychain-item`; map only the item NAME |
|
|
168
170
|
| - | Git Identity | Stored in preferences JSON | Not Keychain | Written to preferences by setup Step 4 |
|
|
@@ -104,6 +104,7 @@ another stack's toolkit in its place.
|
|
|
104
104
|
| `design_*` (`design_visual_compare`, `design_ui_geometry`, `design_report`, ...) | a built screen has to be compared against its design |
|
|
105
105
|
| `ios_app_store_audit` | a package is heading for review |
|
|
106
106
|
| `ios_testflight_validate` | validating a build before upload |
|
|
107
|
+
| `store_*` (`store_status`, `store_asc_*`, `store_play_*`, `store_search` / `store_call`) | a question about App Store Connect or Google Play data: builds and build numbers, reviews, subscriptions, tracks, vitals. Keys come from the Keychain via the store reference file `/multi-agent:setup` writes; `store_status` says which store resolves. Writes return a preview until `confirm: true` and are refused unattended |
|
|
107
108
|
|
|
108
109
|
Registered at user scope by the installer, and preserved by uninstall - the tools are
|
|
109
110
|
useful with no pipeline at all. If it is not registered the tools simply are not
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# App Store Connect Onboarding (setup Step 3b)
|
|
2
2
|
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [API key](#api-key)
|
|
5
|
+
- [Apple ID + app-specific password](#apple-id-app-specific-password)
|
|
6
|
+
- [Multi-provider accounts](#multi-provider-accounts)
|
|
7
|
+
- [Verify + expiry](#verify-expiry)
|
|
8
|
+
<!-- /toc -->
|
|
9
|
+
|
|
3
10
|
Loaded on demand by `/multi-agent:setup` Step 3b (iOS only, optional). The SKILL.md carries the step intro; this file is the full flow.
|
|
4
11
|
|
|
5
12
|
Runs inside Step 3 alongside the other missing credentials, not as a late add-on:
|
|
@@ -7,24 +14,25 @@ a user who already has an App Store Connect credential in their keychain gets it
|
|
|
7
14
|
mapped by Step 1 discovery like any other token, and only the genuinely missing
|
|
8
15
|
pieces reach this flow.
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
None of the five entries goes through the clipboard Token Save Flow: four hold
|
|
18
|
+
something that is not a pasteable secret, and the one secret arrives as a file.
|
|
12
19
|
|
|
13
20
|
| Entry | Holds | Flow |
|
|
14
21
|
|---|---|---|
|
|
15
22
|
| `appstore_connect_key_id` | an identifier | plain value, not a secret; still mapped so it is read through the mapping layer |
|
|
16
23
|
| `appstore_connect_issuer_id` | an identifier | same |
|
|
17
|
-
| `
|
|
24
|
+
| `appstore_connect_private_key` | the full PEM text of `AuthKey_<keyId>.p8` | probed from the file, then stored from the file through stdin; never pasted, never on argv |
|
|
25
|
+
| `appstore_connect_apple_id` | an email address | plain value, not a secret |
|
|
18
26
|
| `appstore_connect_password_item` | a keychain ITEM NAME | the password lives in Apple's own keychain item, referenced as `-p @keychain:<item>` and never read by the pipeline |
|
|
19
27
|
|
|
20
|
-
All
|
|
28
|
+
All five entries are **iOS-only and optional**: skip them all and the pipeline
|
|
21
29
|
still works, it just reports Gate 2 of `/multi-agent:store-ready` as `SKIPPED`,
|
|
22
30
|
never as a pass.
|
|
23
31
|
|
|
24
32
|
The tiers mirror the Figma 3-tier shape: Tier 1 = API key (`appstore_connect_key_id`
|
|
25
|
-
+ `appstore_connect_issuer_id`), Tier 2 = Apple ID
|
|
26
|
-
(`appstore_connect_apple_id` + `appstore_connect_password_item`),
|
|
27
|
-
configured.
|
|
33
|
+
+ `appstore_connect_issuer_id` + `appstore_connect_private_key`), Tier 2 = Apple ID
|
|
34
|
+
+ app-specific password (`appstore_connect_apple_id` + `appstore_connect_password_item`),
|
|
35
|
+
Tier 3 = nothing configured.
|
|
28
36
|
|
|
29
37
|
Ask which tier to configure (picker): **API key** / **Apple ID + app-specific
|
|
30
38
|
password** / **Skip**. Lead with the second when the user says they cannot create
|
|
@@ -32,16 +40,89 @@ an API key: creating one needs an Admin or App Manager role in App Store Connect
|
|
|
32
40
|
while an app-specific password is generated by the account holder at
|
|
33
41
|
`appleid.apple.com` with no team permission at all.
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
store. It must sit in a directory `altool` already searches:
|
|
43
|
+
## API key
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
Every part of the key lives in the Keychain, the `.p8` included. The toolkit's
|
|
46
|
+
`store_asc_*` tools and `ios_testflight_validate` read all three items through the
|
|
47
|
+
store reference file (`~/.config/multi-agent-toolkit/store.json`). Called with
|
|
48
|
+
neither `api_key_id` nor `apple_id`, `ios_testflight_validate` uses that same key
|
|
49
|
+
and hands `altool` a private temporary copy that exists only for the length of the
|
|
50
|
+
call, so no copy under `~/.appstoreconnect/private_keys/` is needed.
|
|
51
|
+
|
|
52
|
+
Map before storing. The three entries in `prefs.global.keychainMapping`
|
|
53
|
+
(`appstore_connect_key_id`, `appstore_connect_issuer_id`,
|
|
54
|
+
`appstore_connect_private_key`) point at their standard names from
|
|
55
|
+
`refs/keychain.md`, or keep an existing mapping when re-onboarding, as Token Save
|
|
56
|
+
Flow Step D does for any key. `set` resolves a logical key through the mapping,
|
|
57
|
+
so every write below goes through the logical key and lands in the item the
|
|
58
|
+
mapping names; an unmapped key would be stored under the bare logical name.
|
|
59
|
+
|
|
60
|
+
1. **Key id and issuer id.** App Store Connect -> Users and Access -> Integrations
|
|
61
|
+
-> App Store Connect API. Both are identifiers; store them as plain values:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
~/.claude/lib/credential-store.sh set appstore_connect_key_id "<key id>"
|
|
65
|
+
~/.claude/lib/credential-store.sh set appstore_connect_issuer_id "<issuer id>"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. **Locate the `.p8`.** Ask for the path to `AuthKey_<keyId>.p8` as a path, never
|
|
69
|
+
its contents. Apple lets the file be downloaded once, so it is usually in
|
|
70
|
+
`~/Downloads` or `~/.appstoreconnect/private_keys/`:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
ls ~/Downloads/AuthKey_*.p8 ~/.appstoreconnect/private_keys/AuthKey_*.p8 2>/dev/null
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
3. **Validate before saving.** A key that is saved first and tried later fails at
|
|
77
|
+
the first store call, long after setup. Probe it against the file:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
node "$HOME/.claude/scripts/probe-store-key.mjs" --asc \
|
|
81
|
+
--key-id "<key id>" --issuer-id "<issuer id>" --p8 "<path to AuthKey_<keyId>.p8>"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It signs a 10-minute ES256 token, reads `GET /v1/apps?limit=1` and prints one
|
|
85
|
+
line, `{ok, store, status, hint}`, with no key material in it. Exit 0 = accepted.
|
|
86
|
+
On exit 1 show the `hint` and stop this tier: `401` means the key id or issuer
|
|
87
|
+
id does not belong to this `.p8`, the key was revoked, or the clock is off;
|
|
88
|
+
`403` means the key's role cannot read apps. Exit 2 is a usage problem (wrong
|
|
89
|
+
path, not a private key). Nothing is saved on a failed probe.
|
|
90
|
+
|
|
91
|
+
4. **Store the key from the file.** A file does not go through the clipboard;
|
|
92
|
+
stdin keeps it off argv and out of shell history:
|
|
42
93
|
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
```bash
|
|
95
|
+
~/.claude/lib/credential-store.sh set appstore_connect_private_key - < "<path to AuthKey_<keyId>.p8>"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
5. **Write the store reference file.**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
node "$HOME/.claude/scripts/write-store-config.mjs"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It names the three Keychain items in `~/.config/multi-agent-toolkit/store.json`
|
|
105
|
+
(0600, directory 0700), after checking each item exists without reading it.
|
|
106
|
+
`store_status` in the toolkit then reports App Store Connect as configured from
|
|
107
|
+
the Keychain.
|
|
108
|
+
|
|
109
|
+
6. **Offer to delete the `.p8` from disk** (picker, default keep). The key is in
|
|
110
|
+
the Keychain now, and a plain file in `~/Downloads` is the copy most likely to
|
|
111
|
+
leak. Apple does not let it be downloaded again, so the user decides:
|
|
112
|
+
|
|
113
|
+
- `question` / `description` in `outputLanguage`, for example "The key is now in
|
|
114
|
+
the Keychain. Delete the .p8 file from disk?"
|
|
115
|
+
- `header`: `P8 file`
|
|
116
|
+
- options: `{label: "Keep file", description: "<outputLanguage: leave the file where it is>"}`
|
|
117
|
+
(first, default) / `{label: "Delete file", description: "<outputLanguage: remove it; the Keychain copy is the only one left>"}`
|
|
118
|
+
|
|
119
|
+
On **Delete file**, `rm "<path>"` that one file and nothing else. On **Keep
|
|
120
|
+
file**, say where it is.
|
|
121
|
+
|
|
122
|
+
## Apple ID + app-specific password
|
|
123
|
+
|
|
124
|
+
Use Apple's own keychain helper. The secret never enters chat and never becomes a
|
|
125
|
+
shell argument, per the Token Save Flow rule:
|
|
45
126
|
|
|
46
127
|
```bash
|
|
47
128
|
# the user exports AC_PASSWORD_ONCE in their own shell, for this one command
|
|
@@ -51,15 +132,20 @@ xcrun altool --store-password-in-keychain-item "<item-name>" \
|
|
|
51
132
|
|
|
52
133
|
Then map only `<item-name>` as `appstore_connect_password_item`.
|
|
53
134
|
|
|
54
|
-
|
|
135
|
+
## Multi-provider accounts
|
|
136
|
+
|
|
137
|
+
A corporate Apple ID often belongs to several
|
|
55
138
|
providers, and `altool` fails opaquely without one. Resolve it once with
|
|
56
139
|
`ios_testflight_validate({list_providers: true, <credentials just configured>})`
|
|
57
140
|
and store the answer under
|
|
58
141
|
`prefs.projects[<key>].appStoreConnect.providerPublicId` - per-project, since a
|
|
59
142
|
user can ship for more than one team.
|
|
60
143
|
|
|
61
|
-
|
|
62
|
-
tier. A credential that resolves but is rejected (401/403) follows the
|
|
63
|
-
Expired-token decision in `refs/keychain.md` Rule 1 - Regenerate / Use a
|
|
64
|
-
different token / Skip and continue - never a silent drop.
|
|
144
|
+
## Verify + expiry
|
|
65
145
|
|
|
146
|
+
For the API key, `store_status({probe: true})` makes one read-only call and says
|
|
147
|
+
whether it worked; for either tier, re-run the `list_providers` probe and report
|
|
148
|
+
the resolved tier. A credential that resolves but is rejected (401/403) follows the
|
|
149
|
+
Expired-token decision in `refs/keychain.md` Rule 1 - Regenerate / Use a
|
|
150
|
+
different token / Skip and continue - never a silent drop. Regenerating the API
|
|
151
|
+
key repeats steps 2 to 5 with the new file.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Google Play Onboarding (setup Step 3d)
|
|
2
|
+
|
|
3
|
+
Loaded on demand by `/multi-agent:setup` Step 3d (Android only, optional). The SKILL.md carries the step intro; this file is the full flow.
|
|
4
|
+
|
|
5
|
+
Runs inside Step 3, only when the target repo is Android, the same way Step 3b
|
|
6
|
+
runs only for iOS. One entry, `google_play`: a Google Cloud service-account JSON,
|
|
7
|
+
stored exactly as Google issued it. The toolkit's `store_play_*` tools
|
|
8
|
+
authenticate with it through the store reference file, and it is what lets
|
|
9
|
+
`/multi-agent:store-ready` hand Android Gate 2 to Google instead of reporting it
|
|
10
|
+
`SKIPPED`.
|
|
11
|
+
|
|
12
|
+
## Ask
|
|
13
|
+
|
|
14
|
+
Picker: **Add Google Play** / **Skip**. `question` and `description` in
|
|
15
|
+
`outputLanguage`; `label` and `header` (`Google Play`) English. Skip writes
|
|
16
|
+
nothing.
|
|
17
|
+
|
|
18
|
+
## What the account needs
|
|
19
|
+
|
|
20
|
+
Say this before asking for the file; a key that authenticates but reaches nothing
|
|
21
|
+
is the common failure.
|
|
22
|
+
|
|
23
|
+
1. Play Console -> Setup -> API access: link a Google Cloud project, create (or
|
|
24
|
+
pick) a service account and download its JSON key.
|
|
25
|
+
2. Play Console -> Users and permissions: invite the service account's
|
|
26
|
+
`client_email` with permissions for the apps it should read.
|
|
27
|
+
3. In that Google Cloud project, enable the **Google Play Android Developer API**
|
|
28
|
+
and the **Google Play Developer Reporting API**.
|
|
29
|
+
|
|
30
|
+
## Validate before saving
|
|
31
|
+
|
|
32
|
+
Ask for the JSON's **path**, never its contents; a file does not go through the
|
|
33
|
+
clipboard or the chat.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node "$HOME/.claude/scripts/probe-store-key.mjs" --play --service-account "<path to service-account.json>"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
It exchanges a signed assertion at `oauth2.googleapis.com` for the
|
|
40
|
+
`androidpublisher` and `playdeveloperreporting` scopes, reads one page of
|
|
41
|
+
`apps:search` on the reporting API, and prints `{ok, store, status, hint}` with no
|
|
42
|
+
key material. Exit 0 = accepted. On exit 1 show the `hint` and save nothing:
|
|
43
|
+
|
|
44
|
+
| Status | Meaning | Fix |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `403` with `SERVICE_DISABLED` | one of the two APIs is off in the account's Cloud project | enable both APIs (step 3 above), probe again |
|
|
47
|
+
| any other `403` | the account is not invited in Play Console, or holds no app permission | step 2 above |
|
|
48
|
+
| `400` / `401` at the token step | the key was deleted or disabled, or the clock is off | generate a new key for the account |
|
|
49
|
+
|
|
50
|
+
Exit 2 is a usage problem (wrong path, not a service-account file).
|
|
51
|
+
|
|
52
|
+
## Save
|
|
53
|
+
|
|
54
|
+
Map `keychainMapping.google_play` first: the standard name
|
|
55
|
+
`${USER}_GooglePlay_Access_Json`, or the existing mapping when re-onboarding
|
|
56
|
+
(Token Save Flow Step D). `set` resolves a logical key through the mapping, and an
|
|
57
|
+
unmapped one would be stored under the bare logical name. Then:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
~/.claude/lib/credential-store.sh set google_play - < "<path to service-account.json>"
|
|
61
|
+
node "$HOME/.claude/scripts/write-store-config.mjs"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The writer names the item in `~/.config/multi-agent-toolkit/store.json` (0600,
|
|
65
|
+
directory 0700) after checking it exists without reading it. `store_status` in the
|
|
66
|
+
toolkit then reports Google Play as configured from the Keychain, and
|
|
67
|
+
`store_status({probe: true})` makes one read-only call to confirm it.
|
|
68
|
+
|
|
69
|
+
The JSON stays on disk unless the user asks otherwise: Google issues a new key on
|
|
70
|
+
request, so the file is not the only copy the way a `.p8` is.
|
|
@@ -208,7 +208,15 @@
|
|
|
208
208
|
},
|
|
209
209
|
"appstore_connect_issuer_id": {
|
|
210
210
|
"type": ["string", "null"],
|
|
211
|
-
"description": "App Store Connect API issuer ID. Required alongside appstore_connect_key_id; either alone leaves Tier 1 unusable and the resolver says so instead of silently dropping to Tier 2. The .p8 private key is
|
|
211
|
+
"description": "App Store Connect API issuer ID. Required alongside appstore_connect_key_id; either alone leaves Tier 1 unusable and the resolver says so instead of silently dropping to Tier 2. The .p8 private key is stored in the Keychain as well, under appstore_connect_private_key; the store reference file written by write-store-config.mjs names all three items so the toolkit's store_* tools and ios_testflight_validate read the key from there."
|
|
212
|
+
},
|
|
213
|
+
"appstore_connect_private_key": {
|
|
214
|
+
"type": ["string", "null"],
|
|
215
|
+
"description": "Keychain item holding the App Store Connect API private key: the full PEM text of AuthKey_<keyId>.p8. Stored from the file through credential-store.sh stdin so the key never reaches argv or the clipboard. The toolkit reads it through the store reference file and writes a short-lived 0600 copy only for tools that read a key from disk."
|
|
216
|
+
},
|
|
217
|
+
"google_play": {
|
|
218
|
+
"type": ["string", "null"],
|
|
219
|
+
"description": "Keychain item holding the Google Play service-account JSON, stored exactly as Google issued it. Read by the toolkit's store_play_* tools through the store reference file. The account must be invited in Play Console with app permissions, and the Google Play Android Developer API and the Google Play Developer Reporting API must be enabled in its Google Cloud project."
|
|
212
220
|
},
|
|
213
221
|
"appstore_connect_apple_id": {
|
|
214
222
|
"type": ["string", "null"],
|
|
@@ -752,7 +752,38 @@
|
|
|
752
752
|
},
|
|
753
753
|
"registerMethod": {
|
|
754
754
|
"type": "object",
|
|
755
|
-
"description": "Method a registrar implements.",
|
|
755
|
+
"description": "Method a registrar implements to perform the registration.",
|
|
756
|
+
"additionalProperties": false,
|
|
757
|
+
"required": ["value", "source", "evidence", "confidence"],
|
|
758
|
+
"properties": {
|
|
759
|
+
"value": {
|
|
760
|
+
"anyOf": [
|
|
761
|
+
{
|
|
762
|
+
"type": "string",
|
|
763
|
+
"minLength": 1
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"type": "null"
|
|
767
|
+
}
|
|
768
|
+
]
|
|
769
|
+
},
|
|
770
|
+
"source": {
|
|
771
|
+
"$ref": "#/$defs/source"
|
|
772
|
+
},
|
|
773
|
+
"evidence": {
|
|
774
|
+
"$ref": "#/$defs/evidence"
|
|
775
|
+
},
|
|
776
|
+
"confidence": {
|
|
777
|
+
"$ref": "#/$defs/confidence"
|
|
778
|
+
},
|
|
779
|
+
"note": {
|
|
780
|
+
"type": "string"
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
"validateMethod": {
|
|
785
|
+
"type": "object",
|
|
786
|
+
"description": "Method a registrar implements to check that what its units read is wired (validate, verify, check). Optional: profiles derived before it existed omit it.",
|
|
756
787
|
"additionalProperties": false,
|
|
757
788
|
"required": ["value", "source", "evidence", "confidence"],
|
|
758
789
|
"properties": {
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
rmSync,
|
|
50
50
|
realpathSync,
|
|
51
51
|
} from "node:fs";
|
|
52
|
-
import { join, dirname } from "node:path";
|
|
52
|
+
import { basename, join, dirname } from "node:path";
|
|
53
53
|
import { homedir } from "node:os";
|
|
54
54
|
import { execFileSync } from "node:child_process";
|
|
55
55
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -92,6 +92,7 @@ const CHECK_IDS = [
|
|
|
92
92
|
"hook-coverage",
|
|
93
93
|
"credential-mapping",
|
|
94
94
|
"credential-liveness",
|
|
95
|
+
"store-reference",
|
|
95
96
|
"embedded-credentials",
|
|
96
97
|
"task-tools",
|
|
97
98
|
"mcp-registration",
|
|
@@ -584,6 +585,94 @@ function checkCredentials(inv) {
|
|
|
584
585
|
ok("credential-liveness");
|
|
585
586
|
}
|
|
586
587
|
|
|
588
|
+
// The toolkit's store_* tools find the App Store Connect and Google Play keys
|
|
589
|
+
// through a reference file naming Keychain services, written by
|
|
590
|
+
// write-store-config.mjs from keychainMapping. A mapping the file does not
|
|
591
|
+
// reflect means the keys sit in the Keychain and every store call still says
|
|
592
|
+
// "not onboarded". Nothing mapped is nothing claimed, so that case is OK.
|
|
593
|
+
const STORE_OF_KEY = {
|
|
594
|
+
appstore_connect_key_id: "appStoreConnect",
|
|
595
|
+
appstore_connect_issuer_id: "appStoreConnect",
|
|
596
|
+
appstore_connect_private_key: "appStoreConnect",
|
|
597
|
+
google_play: "googlePlay",
|
|
598
|
+
};
|
|
599
|
+
const STORE_LABELS = { appStoreConnect: "App Store Connect", googlePlay: "Google Play" };
|
|
600
|
+
|
|
601
|
+
// The host tree this doctor was installed into (~/.claude, ~/.copilot or
|
|
602
|
+
// ~/.codex); run from a checkout, the Claude tree.
|
|
603
|
+
const HOST_TREE = [".claude", ".copilot", ".codex"].includes(basename(dirname(HERE)))
|
|
604
|
+
? dirname(HERE)
|
|
605
|
+
: CLAUDE;
|
|
606
|
+
|
|
607
|
+
function checkStoreReference() {
|
|
608
|
+
const prefsFile = join(CLAUDE, "multi-agent-preferences.json");
|
|
609
|
+
const km = readJson(prefsFile)?.global?.keychainMapping || {};
|
|
610
|
+
const mapped = Object.keys(STORE_OF_KEY).filter((k) => typeof km[k] === "string" && km[k].trim());
|
|
611
|
+
if (!mapped.length) {
|
|
612
|
+
ok("store-reference");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const writer = join(HOST_TREE, "scripts", "write-store-config.mjs");
|
|
616
|
+
if (!existsSync(writer)) {
|
|
617
|
+
skip("store-reference", "write-store-config.mjs is not installed");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
let res;
|
|
621
|
+
try {
|
|
622
|
+
const out = execFileSync(process.execPath, [writer, "--check"], {
|
|
623
|
+
encoding: "utf8",
|
|
624
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
625
|
+
env: {
|
|
626
|
+
...process.env,
|
|
627
|
+
MULTI_AGENT_PREFS: prefsFile,
|
|
628
|
+
CRED_STORE: join(HOST_TREE, "lib", "credential-store.sh"),
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
res = JSON.parse(out);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
try {
|
|
634
|
+
res = JSON.parse(String(e.stdout || ""));
|
|
635
|
+
} catch {
|
|
636
|
+
res = null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (!res || res.error) {
|
|
640
|
+
skip("store-reference", res?.error || "write-store-config.mjs --check produced no verdict");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const fix = `run node "$HOME/${basename(HOST_TREE)}/scripts/write-store-config.mjs"`;
|
|
644
|
+
if (res.state !== "current") {
|
|
645
|
+
const ready = Object.entries(res.stores || {})
|
|
646
|
+
.filter(([, v]) => v.included)
|
|
647
|
+
.map(([k]) => STORE_LABELS[k] || k);
|
|
648
|
+
report(
|
|
649
|
+
"store-reference",
|
|
650
|
+
"WARN",
|
|
651
|
+
`store keys are in the Keychain but the store reference file ${res.state === "missing" ? "is missing" : "is stale"} (${res.path}), so store_* tools cannot find ${ready.join(" and ") || "them"}`,
|
|
652
|
+
fix,
|
|
653
|
+
);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const partial = Object.entries(res.stores || {}).filter(
|
|
657
|
+
([store, v]) => !v.included && mapped.some((k) => STORE_OF_KEY[k] === store),
|
|
658
|
+
);
|
|
659
|
+
if (partial.length) {
|
|
660
|
+
report(
|
|
661
|
+
"store-reference",
|
|
662
|
+
"INFO",
|
|
663
|
+
partial
|
|
664
|
+
.map(
|
|
665
|
+
([store, v]) =>
|
|
666
|
+
`${STORE_LABELS[store] || store} is partly onboarded, missing ${v.missing.join(", ")}`,
|
|
667
|
+
)
|
|
668
|
+
.join("; "),
|
|
669
|
+
`run ${hostCommand("setup", HERE)}`,
|
|
670
|
+
);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
ok("store-reference");
|
|
674
|
+
}
|
|
675
|
+
|
|
587
676
|
function checkEmbeddedCredentials() {
|
|
588
677
|
// The value never enters a shell variable or this process's output. What is
|
|
589
678
|
// reported is the repo, the config key, the host, a shape label and a length
|
|
@@ -1079,6 +1168,7 @@ function main() {
|
|
|
1079
1168
|
checkIdentity();
|
|
1080
1169
|
checkHookCoverage();
|
|
1081
1170
|
checkCredentials(credentialInventory());
|
|
1171
|
+
checkStoreReference();
|
|
1082
1172
|
checkEmbeddedCredentials();
|
|
1083
1173
|
checkTaskTools();
|
|
1084
1174
|
checkMcpRegistration();
|
|
@@ -294,6 +294,11 @@ $folded"
|
|
|
294
294
|
# +0.25. Clipping keeps one unusual pair from outweighing the rest. An
|
|
295
295
|
# all-caps segment of up to five letters inside a token that also has
|
|
296
296
|
# lowercase letters is an acronym (URL, HTTP, JSON) and is not scored.
|
|
297
|
+
# Version markers (v1, V2), digit runs and "/" separate segments too, so
|
|
298
|
+
# a versioned API name
|
|
299
|
+
# (ReportingV1beta1QueryCrashRateMetricSetRequest) or a short REST path
|
|
300
|
+
# (/v1/inAppPurchaseOfferCodes) is read as the words it is made of; a
|
|
301
|
+
# token that is mostly digits is never word-shaped.
|
|
297
302
|
BEGIN {
|
|
298
303
|
rows = "06655352403757150767433232 52015000500600400421400010 " \
|
|
299
304
|
"60306006504400600515500040 50026021600313500420301030 " \
|
|
@@ -322,13 +327,17 @@ $folded"
|
|
|
322
327
|
pair_score += PAIR[substr(l, j - 1, 2)];
|
|
323
328
|
}
|
|
324
329
|
}
|
|
325
|
-
function wordlike(t, n, i, c, p, nx, seg, nseg, total, maxlen, split_here, has_lower) {
|
|
326
|
-
if (t !~ /^[A-Za-
|
|
330
|
+
function wordlike(t, n, i, c, p, nx, seg, nseg, total, maxlen, split_here, has_lower, digits) {
|
|
331
|
+
if (t !~ /^[A-Za-z0-9_\/-]+$/) return 0;
|
|
332
|
+
digits = t; gsub(/[^0-9]/, "", digits);
|
|
333
|
+
if (length(digits) * 8 > length(t)) return 0;
|
|
334
|
+
gsub(/[vV][0-9]+/, "_", t);
|
|
327
335
|
has_lower = (t ~ /[a-z]/);
|
|
328
336
|
pairs = 0; pair_score = 0;
|
|
329
337
|
n = length(t); seg = ""; nseg = 0; total = 0; maxlen = 0;
|
|
330
338
|
for (i = 1; i <= n + 1; i++) {
|
|
331
339
|
c = (i <= n) ? substr(t, i, 1) : "_";
|
|
340
|
+
if (c ~ /[0-9\/]/) c = "_";
|
|
332
341
|
split_here = 0;
|
|
333
342
|
if (c == "_" || c == "-") split_here = 1;
|
|
334
343
|
else if (seg != "" && c ~ /[A-Z]/) {
|