Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/skills/plugin-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ Search the source tree:
- `settings.gradle.kts` must NOT include `mavenLocal()` in `dependencyResolutionManagement` (it's also iffy in `pluginManagement`).
- All dependency versions pinned — no `+`, `latest.release`, or unbounded ranges.
- Phase 1 already verified the build works from a clean checkout. Cite the result here.
- **Provenance record** (ADFA-5256): `unzip -p <plugin>/build/plugin/<name>.cgp assets/cgp-build.properties` on the artifact Phase 1 built. The file is written unconditionally by any builder from 26.36 on, so its **absence** means the submission was built with a pre-5256 builder — note it, do not fail on it. When present, read `revision_source`: `git`, `git-dir`, `explicit` or `env:*` all satisfy "rebuildable from the linked source". `none` (i.e. `revision=unknown`) means the artifact cannot be traced back to a commit → **Partial**, and tell the author to either build from a checkout or declare `pluginBuilder { pluginVcsRevision = "..." }` for a source-archive distribution.
- **`+dirty` in a submitted artifact** → **Partial**: the tree had uncommitted changes in the plugin's own directory, so the linked source does not reproduce it.
- **Manifest wiring** is optional but graded together with the above: if `src/main/AndroidManifest.xml` declares `plugin.vcs_revision` / `plugin.build_timestamp`, both must use `${pluginVcsRevision}` / `${pluginBuildTimestamp}` verbatim — a hardcoded value defeats the point and is a **Fail**. If neither is declared, the record still exists in the archive but the IDE cannot show it; note it as a recommendation, not a defect.
- Do **not** flag a revision that differs from the linked repo's `HEAD` — an artifact built one commit before a `libs/` refresh is normal in this repo's release pipeline (see CLAUDE.md, *Build provenance*). Cross-check `libs_revision` instead.

#### 6.4 Native binaries
- `find src -name '*.so'`, look for `jniLibs/`, `externalNativeBuild`, NDK config.
Expand Down
35 changes: 32 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,40 @@ A plugin is an Android *application* module (despite installing as a library) wi

1. **`build.gradle.kts`** applies `com.android.application`, `org.jetbrains.kotlin.android`, and `com.itsaky.androidide.plugins.build`. Configures `pluginBuilder { pluginName = "..." }`. Uses `compileOnly(files("../../libs/plugin-api.jar"))` — never `implementation`.
2. **`settings.gradle.kts`** declares the jars it needs on the buildscript classpath plus AGP and Kotlin.
3. **`src/main/AndroidManifest.xml`** declares plugin identity as `<meta-data>` entries on `<application>`: `plugin.id`, `plugin.name`, `plugin.version` (resolved from `${pluginVersion}`), `plugin.description`, `plugin.author`, `plugin.main_class`, `plugin.min_ide_version`, and optional `plugin.permissions`.
3. **`src/main/AndroidManifest.xml`** declares plugin identity as `<meta-data>` entries on `<application>`: `plugin.id`, `plugin.name`, `plugin.version` (resolved from `${pluginVersion}`), `plugin.description`, `plugin.author`, `plugin.main_class`, `plugin.min_ide_version`, and optional `plugin.permissions`. Optionally `plugin.vcs_revision` / `plugin.build_timestamp` — see **Build provenance** below.
4. **Main class** implements `com.itsaky.androidide.plugins.IPlugin`. Lifecycle: `initialize(PluginContext) → activate() → deactivate() → dispose()`. Services are obtained via `context.services.get(SomeService::class.java)` (e.g. `IdeBuildService` for build hooks). Android `Context` is `context.androidContext`.

Available permission strings (declared comma-separated in `plugin.permissions`): `filesystem.read`, `filesystem.write`, `network.access`, `system.commands`, `ide.settings`, `project.structure`.

### Build provenance (`plugin.vcs_revision`, `cgp-build.properties`)

Every `.cgp` records the commit it was built from, so a crash report or a support question traces back to source (ADFA-5256). The builder resolves it once per build and publishes it three ways: two `<meta-data>` entries, `assets/cgp-build.properties` inside the archive, and the IDE's plugin details dialog.

Manifests opt in by referencing the placeholders — the builder never injects `<meta-data>` on your behalf:

```xml
<meta-data android:name="plugin.vcs_revision" android:value="${pluginVcsRevision}" />
<meta-data android:name="plugin.build_timestamp" android:value="${pluginBuildTimestamp}" />
```

**This is a hard build-time coupling to `libs/gradle-plugin.jar`.** A manifest that references a placeholder the builder does not define fails the manifest merger outright (*"requires a placeholder substitution but no value ... is provided"*), and all plugins resolve the builder from the single committed jar. So a manifest may only adopt these **after** the builder change is merged in CoGo and the **Update libs from CodeOnTheGo** Action has refreshed `libs/`. Never the other way round. The same coupling hits on-device builders, whose builder jar ships in `plugin-maven-repo.zip` and refreshes only with a CoGo **app release** — a plugin referencing these cannot be built inside an older CoGo at all.

Read the record out of a built artifact:

```sh
unzip -p <plugin>/build/plugin/<name>.cgp assets/cgp-build.properties
```

`revision_source` says how the revision was found, in the order the builder tries: `explicit` (you set `pluginBuilder { pluginVcsRevision = "..." }`) → `env:<VAR>` (`PLUGIN_VCS_REVISION`, `GITHUB_SHA`, `CI_COMMIT_SHA`, `GIT_COMMIT`) → `git` → `git-dir` (reads `.git` directly; this is the on-device path, since CoGo ships JGit in-process and no `git` binary) → `none`, which means `revision=unknown`. `+dirty` is appended when the plugin's **own** directory has uncommitted changes; the check is scoped to that directory so a `libs/` refresh elsewhere in the tree does not flag the build.

`timestamp` is the committer date of that revision in UTC, not the wall clock, so two builds of one commit produce a byte-identical `.cgp` (see CoGo's ADR-0012). That holds only where the builder could reach a `git` binary; it falls back to the clock and says so with `timestamp_source=wall-clock`, and in that case the stamp lands in the version string too, so the artifact is **not** reproducible. On device it is always the fallback — CoGo ships no `git` — and under `--configuration-cache` the clock reading additionally freezes into the cached configuration.

`+dirty` has one systemic cause worth designing against: **a build-time download must land on a gitignored path.** A `downloadAssets` task that overwrites a git-tracked file (`ndk-installer` shipped a committed placeholder `ndk-cmake.tar.xz` until it was untracked) dirties the plugin directory on every build, so every artifact it ever produces records `+dirty` and no build of that plugin is traceable to a clean commit. Both download plugins now fetch onto ignored paths (`plugins/NDK-Installer/.gitignore`, `ai-literacy-course/.gitignore`); keep it that way when adding a new one.

`libs_revision` records which CoGo commit produced the jars the plugin was compiled against. The builder cannot see that checkout, so `scripts/update-libs.sh` exports `PLUGIN_LIBS_REVISION` before the build loop — both Actions workflows inherit it through the script. Note that under **Update libs from CodeOnTheGo** the plugin's own `revision` is the commit *before* the `chore: update libs` commit, because plugins are built before that commit is created; `libs_revision` is what pins the pairing.

`scripts/update-libs.sh` asserts the record after each `assemblePlugin`: a `.cgp` missing `assets/cgp-build.properties`, missing any required key, or disagreeing with the exported `PLUGIN_LIBS_REVISION` fails the run. `revision=unknown`, `+dirty` and `timestamp_source=wall-clock` warn instead — all three are legitimate off-CI (no `.git`, no `git` binary). Both Actions workflows build through the script, so this covers them without a per-workflow check.

### In-app help wiring (tooltips + Tier 3, `DocumentationExtension`)

Every plugin with UI implements `com.itsaky.androidide.plugins.extensions.DocumentationExtension`. This wiring is fixed and foundational — get **all** of it right or the tooltip renders the literal string **`n/a`** at runtime. The build stays green and the manifest looks fine, so **only device long-press testing catches a mistake** (this bit us once). All symbols are in `plugin-api.jar`.
Expand Down Expand Up @@ -121,9 +150,9 @@ This is intentional — the `application`-as-library packaging trips those check

### Asset downloads (rare)

Some plugins (`ndk-installer`, `ai-literacy-course`) register a `downloadAssets` task that fetches large files at build time with pinned-MD5 verification. These assets are **not committed to git** (e.g. `ai-literacy-course` pulls a ~110 MB course ZIP plus `pdfjs.zip`). `scripts/update-libs.sh` runs `downloadAssets` automatically before `assemblePlugin` when the build file references it.
Some plugins (`ndk-installer`, `ai-literacy-course`) register a `downloadAssets` task that fetches large files at build time with pinned-MD5 verification. These assets are **not committed to git** — each plugin gitignores its own download paths (`ai-literacy-course` pulls a ~110 MB course ZIP plus `pdfjs.zip`; `ndk-installer` pulls `ndk-cmake.tar.xz`). Committing one, even as a placeholder, makes every build dirty — see **Build provenance** above. `scripts/update-libs.sh` runs `downloadAssets` automatically before `assemblePlugin` when the build file references it.

**Gotcha: a bare `./gradlew assemblePlugin` does NOT run `downloadAssets` and does not warn when the assets are missing** — it silently packages a broken `.cgp` (e.g. a course with no PDF viewer). When building such a plugin by hand, run `./gradlew downloadAssets assemblePlugin` (or the script), and confirm the expected files exist under `src/main/assets/` (or `unzip -l` the `.cgp`) before trusting it.
**Gotcha: a bare `./gradlew assemblePlugin` does NOT run `downloadAssets` and does not warn when the assets are missing** — it silently packages a broken `.cgp` (e.g. a course with no PDF viewer). When building such a plugin by hand, run `./gradlew downloadAssets` and then `./gradlew assemblePlugin` as two separate invocations (or use the script, which does exactly that). Combining them in one invocation fails: `downloadAssets` declares an output inside `src/main/assets`, which Gradle sees as an undeclared dependency of `mergeReleaseAssets`. Then confirm the expected files exist under `src/main/assets/` (or `unzip -l` the `.cgp`) before trusting it. `ndk-installer` is the exception: its asset merge fails outright when `ndk-cmake.tar.xz` is absent, rather than shipping an NDK-less plugin.

### One-time on-device install markers

Expand Down
10 changes: 10 additions & 0 deletions ai-agent-gemini/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
android:name="plugin.version"
android:value="${pluginVersion}" />

<!-- Build provenance (ADFA-5256): the commit and commit-date this .cgp was
built from. Both placeholders are always supplied by the plugin builder. -->
<meta-data
android:name="plugin.vcs_revision"
android:value="${pluginVcsRevision}" />

<meta-data
android:name="plugin.build_timestamp"
android:value="${pluginBuildTimestamp}" />

<meta-data
android:name="plugin.description"
android:value="Google Gemini API inference backend for AI Core. Requires a Gemini API key, entered under Preferences &gt; Configuration &gt; Agent." />
Expand Down
4 changes: 2 additions & 2 deletions ai-agent-local/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ android {
applicationId = "com.itsaky.androidide.plugins.aiagentlocal"
minSdk = 33
targetSdk = 36
versionCode = 2
versionName = "1.0.1"
versionCode = 3
versionName = "1.1.0"

ndk {
abiFilters += listOf("arm64-v8a")
Expand Down
10 changes: 10 additions & 0 deletions ai-agent-local/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
android:name="plugin.version"
android:value="${pluginVersion}" />

<!-- Build provenance (ADFA-5256): the commit and commit-date this .cgp was
built from. Both placeholders are always supplied by the plugin builder. -->
<meta-data
android:name="plugin.vcs_revision"
android:value="${pluginVcsRevision}" />

<meta-data
android:name="plugin.build_timestamp"
android:value="${pluginBuildTimestamp}" />

<meta-data
android:name="plugin.description"
android:value="On-device .gguf inference backend for AI Core, powered by a bundled llama.cpp build. Nothing leaves the device." />
Expand Down
10 changes: 10 additions & 0 deletions ai-agent-mcp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
android:name="plugin.version"
android:value="${pluginVersion}" />

<!-- Build provenance (ADFA-5256): the commit and commit-date this .cgp was
built from. Both placeholders are always supplied by the plugin builder. -->
<meta-data
android:name="plugin.vcs_revision"
android:value="${pluginVcsRevision}" />

<meta-data
android:name="plugin.build_timestamp"
android:value="${pluginBuildTimestamp}" />

<meta-data
android:name="plugin.description"
android:value="Connects the Agent to Model Context Protocol servers, so their tools appear alongside the built-in ones. Configure servers under Preferences &gt; Configuration &gt; MCP servers." />
Expand Down
10 changes: 10 additions & 0 deletions ai-agent-openai/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
android:name="plugin.version"
android:value="${pluginVersion}" />

<!-- Build provenance (ADFA-5256): the commit and commit-date this .cgp was
built from. Both placeholders are always supplied by the plugin builder. -->
<meta-data
android:name="plugin.vcs_revision"
android:value="${pluginVcsRevision}" />

<meta-data
android:name="plugin.build_timestamp"
android:value="${pluginBuildTimestamp}" />

<meta-data
android:name="plugin.description"
android:value="OpenAI-compatible inference backend for AI Core. Talks to OpenAI by default, or to any server that speaks the same chat/completions protocol — Ollama, LM Studio, OpenRouter, llama-server — by changing one URL." />
Expand Down
4 changes: 2 additions & 2 deletions ai-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
applicationId = "com.itsaky.androidide.plugins.aicore"
minSdk = 33
targetSdk = 36
versionCode = 4
versionName = "3.0.0"
versionCode = 5
versionName = "3.1.0"
}

buildFeatures {
Expand Down
10 changes: 10 additions & 0 deletions ai-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
android:name="plugin.version"
android:value="${pluginVersion}" />

<!-- Build provenance (ADFA-5256): the commit and commit-date this .cgp was
built from. Both placeholders are always supplied by the plugin builder. -->
<meta-data
android:name="plugin.vcs_revision"
android:value="${pluginVcsRevision}" />

<meta-data
android:name="plugin.build_timestamp"
android:value="${pluginBuildTimestamp}" />

<meta-data
android:name="plugin.description"
android:value="The Agent chat and the LLM inference router. Install it plus at least one backend plugin (AI Agent Local, AI Agent Gemini) to use any AI feature." />
Expand Down
4 changes: 2 additions & 2 deletions plugins/Get-AI-Models/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
applicationId = "org.appdevforall.getaimodels"
minSdk = 21
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
versionCode = 2
versionName = "1.1.0"
}

buildTypes {
Expand Down
10 changes: 10 additions & 0 deletions plugins/Get-AI-Models/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
android:name="plugin.version"
android:value="${pluginVersion}" />

<!-- Build provenance (ADFA-5256): the commit and commit-date this .cgp was
built from. Both placeholders are always supplied by the plugin builder. -->
<meta-data
android:name="plugin.vcs_revision"
android:value="${pluginVcsRevision}" />

<meta-data
android:name="plugin.build_timestamp"
android:value="${pluginBuildTimestamp}" />

<meta-data
android:name="plugin.description"
android:value="Browse a curated catalog of small GGUF language-model files and download them to /sdcard/Download, checksum-verified. Most are fully open; per-entry licences are shown. Download only - running the models is a separate plugin." />
Expand Down
5 changes: 5 additions & 0 deletions plugins/NDK-Installer/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ Thumbs.db

# Test outputs
test-results/

# Fetched at build time by the downloadAssets task — never commit it. A tracked
# copy here would be overwritten by every download, leaving the plugin directory
# permanently dirty and stamping `+dirty` into the provenance of every .cgp.
src/main/assets/ndk-cmake.tar.xz
18 changes: 12 additions & 6 deletions plugins/NDK-Installer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,15 @@ val downloadAssets by tasks.registering {
}
}







// The archive is never committed, so fail loudly instead of packaging an NDK-less .cgp.
val ndkArchiveFile = project.file("src/main/assets/ndk-cmake.tar.xz")
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("Assets") }.configureEach {
doFirst {
if (!ndkArchiveFile.isFile) {
throw GradleException(
"Missing src/main/assets/ndk-cmake.tar.xz. It is fetched at build time and " +
"never committed, so run './gradlew downloadAssets' before assembling."
)
}
}
}
1 change: 0 additions & 1 deletion plugins/NDK-Installer/src/main/assets/ndk-cmake.tar.xz

This file was deleted.

Loading
Loading