Skip to content

Commit 9a0a90c

Browse files
committed
feat(codex): update plugin config, marketplace, add static
1 parent 256c021 commit 9a0a90c

8 files changed

Lines changed: 302 additions & 16 deletions

File tree

.agents/plugins/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"installation": "INSTALLED_BY_DEFAULT",
1515
"authentication": "ON_INSTALL"
1616
},
17-
"category": "Coding"
17+
"category": "Productivity"
1818
}
1919
]
2020
}

.codex-plugin/plugin.json

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
"description": "Make your AI agent code with your project's architecture, rules, and decisions.",
44
"version": "0.4.4",
55
"author": {
6-
"name": "Archcore"
6+
"name": "Archcore",
7+
"url": "https://archcore.ai"
78
},
89
"license": "Apache-2.0",
9-
"homepage": "https://github.com/archcore-ai/plugin",
10+
"homepage": "https://archcore.ai/plugin",
1011
"repository": "https://github.com/archcore-ai/plugin",
1112
"keywords": [
1213
"codex",
@@ -39,17 +40,19 @@
3940
"shortDescription": "Keep Codex aligned with project decisions and architecture.",
4041
"longDescription": "Archcore brings project architecture, decisions, rules, and requirements into Codex through skills, hooks, MCP tools, and documentation-focused agents.",
4142
"developerName": "Archcore",
42-
"category": "Coding",
43-
"capabilities": ["Interactive", "Read", "Write"],
44-
"websiteURL": "https://github.com/archcore-ai/plugin",
45-
"privacyPolicyURL": "https://github.com/archcore-ai/plugin",
46-
"termsOfServiceURL": "https://github.com/archcore-ai/plugin",
43+
"category": "Productivity",
44+
"capabilities": ["Read", "Write"],
45+
"websiteURL": "https://archcore.ai",
46+
"privacyPolicyURL": "https://archcore.ai/privacy",
47+
"termsOfServiceURL": "https://github.com/archcore-ai/plugin/blob/main/docs/TERMS.md",
4748
"defaultPrompt": [
48-
"Review this work against Archcore docs",
49-
"Create an Archcore plan for this change",
50-
"Check whether source edits need doc updates"
49+
"Use Archcore to review this work against project decisions",
50+
"Use Archcore to plan this change",
51+
"Use Archcore to check whether source edits need doc updates"
5152
],
5253
"brandColor": "#2563EB",
54+
"composerIcon": "./assets/icon.png",
55+
"logo": "./assets/logo.png",
5356
"screenshots": []
5457
}
5558
}

assets/icon.png

21.7 KB
Loading

assets/logo.png

21.7 KB
Loading

docs/TERMS.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Terms of Service
2+
3+
_Last updated: 2026-05-17_
4+
5+
These terms govern your use of the Archcore plugin (the "Plugin") distributed in this repository.
6+
7+
## License
8+
9+
The Plugin is licensed under the **Apache License, Version 2.0**. You may use, modify, and redistribute the Plugin in accordance with that license. The full text is available in [LICENSE](../LICENSE).
10+
11+
## Use of the Plugin
12+
13+
- The Plugin integrates with AI coding agents (Claude Code, Codex, Cursor, and similar hosts) and operates on files inside your project.
14+
- You are responsible for the actions you trigger through your AI agent host, including all writes the Plugin performs on your repository.
15+
- The Plugin does not transmit your project contents to Archcore servers. Communication is local between the host and the Plugin's MCP server and hooks.
16+
17+
## No Warranty
18+
19+
The Plugin is provided **"AS IS"**, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. See the Apache License 2.0 for the full disclaimer.
20+
21+
## Limitation of Liability
22+
23+
To the maximum extent permitted by applicable law, in no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Plugin or the use or other dealings in the Plugin.
24+
25+
## Privacy
26+
27+
Data handling is described in the [Privacy Policy](https://archcore.ai/privacy).
28+
29+
## Changes
30+
31+
We may update these terms. Material changes will be reflected by a new "Last updated" date in this document. Continued use of the Plugin after a change constitutes acceptance of the revised terms.
32+
33+
## Contact
34+
35+
Questions about these terms: open an issue at <https://github.com/archcore-ai/plugin/issues>.

docs/release.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,17 @@ Any addition or removal MUST update the workflow at
5454
| `cursor.mcp.json` | Legacy path; must already be gone but stripped defensively. |
5555

5656
Everything else ships: `skills/`, `agents/`, `commands/`, `rules/`,
57-
`hooks/`, `bin/`, `docs/cursor.mcp.example.json`, manifests
58-
(`.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`), MCP configs
57+
`hooks/`, `bin/`, `assets/` (icon + logo for marketplace surfaces),
58+
`docs/cursor.mcp.example.json`, `docs/TERMS.md`, manifests
59+
(`.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`), marketplace
60+
registries (`.agents/plugins/marketplace.json`), MCP configs
5961
(`.mcp.json`, `.codex.mcp.json`), `README.md`, `LICENSE`, `NOTICE`.
6062

63+
> **Note on `assets/`.** Required by `.codex-plugin/plugin.json`
64+
> (`interface.composerIcon`, `interface.logo`). The plugin.json paths are
65+
> relative to plugin root, so the directory must exist alongside the
66+
> manifests in the published `main` tree. Don't strip.
67+
6168
## Cutting a release
6269

6370
1. Bump `version` in all three manifests (`.claude-plugin/plugin.json`,

test/structure/codex-plugin.bats

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ setup() {
3434
jq -e '.interface.shortDescription' < "$file" > /dev/null
3535
jq -e '.interface.longDescription' < "$file" > /dev/null
3636
jq -e '.interface.developerName == "Archcore"' < "$file" > /dev/null
37-
jq -e '.interface.category == "Coding"' < "$file" > /dev/null
38-
jq -e '.interface.capabilities | index("Interactive")' < "$file" > /dev/null
37+
jq -e '.interface.category == "Productivity"' < "$file" > /dev/null
3938
jq -e '.interface.capabilities | index("Read")' < "$file" > /dev/null
4039
jq -e '.interface.capabilities | index("Write")' < "$file" > /dev/null
40+
# Codex docs (developers.openai.com/codex/plugins/build) document only
41+
# "Read" and "Write" as capability values; "Interactive" is not documented
42+
# and was removed to avoid marketplace validation risk.
43+
[ "$(jq '.interface.capabilities | index("Interactive")' < "$file")" = "null" ]
4144
}
4245

4346
@test ".codex-plugin/plugin.json has no legacy top-level UI metadata" {
@@ -120,7 +123,7 @@ setup() {
120123
jq -e '.plugins[0].source.path == "./"' < "$file" > /dev/null
121124
jq -e '.plugins[0].policy.installation == "INSTALLED_BY_DEFAULT"' < "$file" > /dev/null
122125
jq -e '.plugins[0].policy.authentication == "ON_INSTALL"' < "$file" > /dev/null
123-
jq -e '.plugins[0].category == "Coding"' < "$file" > /dev/null
126+
jq -e '.plugins[0].category == "Productivity"' < "$file" > /dev/null
124127
}
125128

126129
@test "legacy .codex-plugin/marketplace.json is absent" {
@@ -183,3 +186,133 @@ setup() {
183186
has_write_path=$(jq -r '.hooks.PostToolUse[]? | select(.matcher | test("^Write|Edit$")) | .matcher' < "$file")
184187
[ -z "$has_write_path" ]
185188
}
189+
190+
# --- Codex marketplace-surface invariants ----------------------------------
191+
# The fields below populate the install card / composer surfaces that Codex
192+
# renders for the plugin. A broken field doesn't fail at runtime — it fails
193+
# silently in the marketplace UI, which is hard to notice from `codex` CLI.
194+
# The tests below pin the contract so accidental edits regress loudly.
195+
196+
png_magic_ok() {
197+
# PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A
198+
[ "$(head -c 8 "$1" | od -An -tx1 | tr -d ' \n')" = "89504e470d0a1a0a" ]
199+
}
200+
201+
@test "assets/icon.png exists and is a valid PNG" {
202+
local file="$PLUGIN_ROOT/assets/icon.png"
203+
[ -f "$file" ] || fail "missing icon: $file"
204+
png_magic_ok "$file" || fail "icon.png is not a valid PNG"
205+
# Sanity: not a stub / empty placeholder
206+
[ "$(wc -c < "$file" | tr -d ' ')" -gt 1000 ] || fail "icon.png suspiciously small"
207+
}
208+
209+
@test "assets/logo.png exists and is a valid PNG" {
210+
local file="$PLUGIN_ROOT/assets/logo.png"
211+
[ -f "$file" ] || fail "missing logo: $file"
212+
png_magic_ok "$file" || fail "logo.png is not a valid PNG"
213+
[ "$(wc -c < "$file" | tr -d ' ')" -gt 1000 ] || fail "logo.png suspiciously small"
214+
}
215+
216+
@test "interface.composerIcon points to an existing file" {
217+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
218+
local rel
219+
rel=$(jq -r '.interface.composerIcon // empty' < "$file")
220+
[ -n "$rel" ] || fail "interface.composerIcon is missing or empty"
221+
[[ "$rel" == ./* ]] || fail "composerIcon must start with './': $rel"
222+
[ -f "$PLUGIN_ROOT/${rel#./}" ] || fail "composerIcon does not resolve: $rel"
223+
}
224+
225+
@test "interface.logo points to an existing file" {
226+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
227+
local rel
228+
rel=$(jq -r '.interface.logo // empty' < "$file")
229+
[ -n "$rel" ] || fail "interface.logo is missing or empty"
230+
[[ "$rel" == ./* ]] || fail "logo must start with './': $rel"
231+
[ -f "$PLUGIN_ROOT/${rel#./}" ] || fail "logo does not resolve: $rel"
232+
}
233+
234+
@test "interface.screenshots is an array; every entry resolves" {
235+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
236+
[ "$(jq -r '.interface.screenshots | type' < "$file")" = "array" ]
237+
local missing=""
238+
local rel
239+
while IFS= read -r rel; do
240+
[ -z "$rel" ] && continue
241+
[[ "$rel" == ./* ]] || { missing="$missing $rel(not-relative)"; continue; }
242+
[ -f "$PLUGIN_ROOT/${rel#./}" ] || missing="$missing $rel"
243+
done < <(jq -r '.interface.screenshots[]?' < "$file")
244+
[ -z "$missing" ] || fail "broken screenshot paths:$missing"
245+
}
246+
247+
@test "interface.brandColor is a valid 6-digit hex color" {
248+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
249+
local color
250+
color=$(jq -r '.interface.brandColor // empty' < "$file")
251+
[[ "$color" =~ ^#[0-9A-Fa-f]{6}$ ]] || fail "brandColor must match ^#[0-9A-Fa-f]{6}$, got: $color"
252+
}
253+
254+
@test "interface.defaultPrompt is a non-empty array of strings" {
255+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
256+
[ "$(jq -r '.interface.defaultPrompt | type' < "$file")" = "array" ]
257+
local count
258+
count=$(jq -r '.interface.defaultPrompt | length' < "$file")
259+
[ "$count" -gt 0 ] || fail "defaultPrompt must contain at least one starter"
260+
# All elements must be strings
261+
local types
262+
types=$(jq -r '.interface.defaultPrompt | map(type) | unique | join(",")' < "$file")
263+
[ "$types" = "string" ] || fail "defaultPrompt entries must all be strings, found types: $types"
264+
# No empty strings
265+
local empties
266+
empties=$(jq -r '.interface.defaultPrompt | map(select(. == "")) | length' < "$file")
267+
[ "$empties" = "0" ] || fail "defaultPrompt contains empty string(s)"
268+
}
269+
270+
@test "interface external URLs are non-empty https://..." {
271+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
272+
local key url
273+
for key in websiteURL privacyPolicyURL termsOfServiceURL; do
274+
url=$(jq -r ".interface.\"$key\" // empty" < "$file")
275+
[ -n "$url" ] || fail "interface.$key is missing or empty"
276+
[[ "$url" == https://* ]] || fail "interface.$key must be https://..., got: $url"
277+
done
278+
}
279+
280+
@test "author.url and homepage are non-empty https URLs" {
281+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
282+
local author_url homepage
283+
author_url=$(jq -r '.author.url // empty' < "$file")
284+
[[ "$author_url" == https://* ]] || fail "author.url must be https://..., got: $author_url"
285+
homepage=$(jq -r '.homepage // empty' < "$file")
286+
[[ "$homepage" == https://* ]] || fail "homepage must be https://..., got: $homepage"
287+
}
288+
289+
@test "docs/TERMS.md exists (referenced by interface.termsOfServiceURL)" {
290+
# termsOfServiceURL points to the raw GitHub blob of this file; if the
291+
# file is deleted, the marketplace card link 404s.
292+
[ -f "$PLUGIN_ROOT/docs/TERMS.md" ] || fail "docs/TERMS.md missing — termsOfServiceURL will 404"
293+
}
294+
295+
@test "category in .codex-plugin/plugin.json matches .agents/plugins/marketplace.json" {
296+
# Drift guard: editing one without the other yields inconsistent marketplace
297+
# presentation (composer says one category, registry says another).
298+
local plugin_cat market_cat
299+
plugin_cat=$(jq -r '.interface.category' < "$PLUGIN_ROOT/.codex-plugin/plugin.json")
300+
market_cat=$(jq -r '.plugins[0].category' < "$PLUGIN_ROOT/.agents/plugins/marketplace.json")
301+
[ "$plugin_cat" = "$market_cat" ] \
302+
|| fail "category drift: plugin.json='$plugin_cat' but marketplace='$market_cat'"
303+
}
304+
305+
@test "every './'-relative path in .codex-plugin/plugin.json resolves" {
306+
# Generic guard: any string value that begins with "./" must point to an
307+
# existing file or directory under PLUGIN_ROOT. Catches typos, renames,
308+
# and accidentally deleted assets across all manifest fields at once.
309+
local file="$PLUGIN_ROOT/.codex-plugin/plugin.json"
310+
local missing="" rel
311+
while IFS= read -r rel; do
312+
[ -z "$rel" ] && continue
313+
if [ ! -e "$PLUGIN_ROOT/${rel#./}" ]; then
314+
missing="$missing $rel"
315+
fi
316+
done < <(jq -r '.. | strings | select(startswith("./"))' < "$file")
317+
[ -z "$missing" ] || fail "unresolved './' paths in plugin.json:$missing"
318+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env bats
2+
# Release pipeline invariants
3+
#
4+
# `.github/workflows/release.yml` synthesizes `main` from `dev` by deleting
5+
# a fixed list of paths (the "blocklist") before force-pushing. The contract
6+
# is mirrored in `docs/release.md`. These tests pin both sides so a change
7+
# to one without the other regresses loudly — and so adding a new marketplace
8+
# surface (e.g. `assets/`) can't silently get stripped.
9+
10+
setup() {
11+
load '../helpers/common'
12+
common_setup
13+
}
14+
15+
YML() {
16+
echo "$PLUGIN_ROOT/.github/workflows/release.yml"
17+
}
18+
19+
is_stripped() {
20+
# Matches `rm -rf <path>` or `rm -f <path>` lines, allowing trailing
21+
# comments. The path is anchored exactly (no prefix matches, so
22+
# `assets` does not match `assets-foo`).
23+
grep -qE "^\s*rm\s+(-rf|-f)\s+${1}(\s|$|#)" "$(YML)"
24+
}
25+
26+
# --- Files that MUST ship to main (no blocklist entry) ---------------------
27+
28+
@test "release.yml does not strip assets/" {
29+
! is_stripped 'assets' || fail "assets/ is in release.yml blocklist — composerIcon/logo will 404 in marketplace"
30+
}
31+
32+
@test "release.yml does not strip docs/TERMS.md" {
33+
! is_stripped 'docs/TERMS\.md' \
34+
|| fail "docs/TERMS.md is in release.yml blocklist — termsOfServiceURL will 404"
35+
}
36+
37+
@test "release.yml does not strip docs/cursor.mcp.example.json" {
38+
! is_stripped 'docs/cursor\.mcp\.example\.json' \
39+
|| fail "docs/cursor.mcp.example.json is in release.yml blocklist"
40+
}
41+
42+
@test "release.yml does not strip .codex-plugin/" {
43+
! is_stripped '\.codex-plugin' \
44+
|| fail ".codex-plugin/ is in release.yml blocklist — Codex install will break"
45+
}
46+
47+
@test "release.yml does not strip .agents/" {
48+
! is_stripped '\.agents' \
49+
|| fail ".agents/ is in release.yml blocklist — Codex marketplace registry will be missing"
50+
}
51+
52+
@test "release.yml does not strip .claude-plugin/ or .cursor-plugin/" {
53+
! is_stripped '\.claude-plugin' || fail ".claude-plugin/ is in release.yml blocklist"
54+
! is_stripped '\.cursor-plugin' || fail ".cursor-plugin/ is in release.yml blocklist"
55+
}
56+
57+
@test "release.yml does not strip skills/, agents/, commands/, rules/, hooks/, bin/" {
58+
! is_stripped 'skills' || fail "skills/ is in blocklist"
59+
! is_stripped 'agents' || fail "agents/ is in blocklist"
60+
! is_stripped 'commands' || fail "commands/ is in blocklist"
61+
! is_stripped 'rules' || fail "rules/ is in blocklist"
62+
! is_stripped 'hooks' || fail "hooks/ is in blocklist"
63+
! is_stripped 'bin' || fail "bin/ is in blocklist"
64+
}
65+
66+
@test "release.yml does not strip top-level MCP configs" {
67+
! is_stripped '\.mcp\.json' || fail ".mcp.json is in blocklist"
68+
! is_stripped '\.codex\.mcp\.json' || fail ".codex.mcp.json is in blocklist"
69+
}
70+
71+
# --- Files that MUST be stripped from main ---------------------------------
72+
73+
@test "release.yml strips .archcore/" {
74+
is_stripped '\.archcore' || fail ".archcore/ MUST be stripped — see docs/release.md"
75+
}
76+
77+
@test "release.yml strips test/" {
78+
is_stripped 'test' || fail "test/ MUST be stripped — bats suite has no place on main"
79+
}
80+
81+
@test "release.yml strips .github/, .claude/, .codex/" {
82+
is_stripped '\.github' || fail ".github/ MUST be stripped"
83+
is_stripped '\.claude' || fail ".claude/ MUST be stripped"
84+
is_stripped '\.codex' || fail ".codex/ MUST be stripped"
85+
}
86+
87+
@test "release.yml strips Makefile and docs/release.md" {
88+
is_stripped 'Makefile' || fail "Makefile MUST be stripped"
89+
is_stripped 'docs/release\.md' || fail "docs/release.md MUST be stripped"
90+
}
91+
92+
@test "release.yml strips reference-materials/" {
93+
is_stripped 'reference-materials' || fail "reference-materials/ MUST be stripped"
94+
}
95+
96+
# --- docs/release.md mirrors workflow contract -----------------------------
97+
98+
@test "docs/release.md mentions assets/ in the ships list" {
99+
# If assets/ is in the workflow but not documented as shipping, future
100+
# readers may not realise it's required by the manifests.
101+
grep -q 'assets/' "$PLUGIN_ROOT/docs/release.md" \
102+
|| fail "docs/release.md must mention assets/ in the 'Everything else ships' section"
103+
}
104+
105+
@test "docs/release.md mentions docs/TERMS.md in the ships list" {
106+
grep -q 'docs/TERMS\.md' "$PLUGIN_ROOT/docs/release.md" \
107+
|| fail "docs/release.md must mention docs/TERMS.md in the 'Everything else ships' section"
108+
}

0 commit comments

Comments
 (0)