Skip to content
cccskills
FreePublished on GitHub

audit-ppmplugin

Statically audit a built `.ppmplugin` before wrap testing. Checks archive layout, manifest compatibility, bundle consistency, Android DEX integrity and SDK leakage, iOS framework structure, native source-to-receiver alignment, and the PCF composite-key/sendAsync transport contract. Reports CRITICAL, WARNING, and INFO findings with fixes routed to the owning stage; never modifies the archive. Requires `jar`; `dexdump` or `strings` improves DEX inspection. Run after /assemble-ppmplugin or standalone on an existing bundle.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md22.6 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

/audit-ppmplugin

The verification gate. /assemble-ppmplugin proves the bundle is well-formed; this skill proves it will actually load and dispatch on the wrap runtime. Most .ppmplugin failures are silent — the bundle uploads fine, then a method returns native module 'X' not loaded, the runtime cannot instantiate the package class, or the upload is rejected with 0x80040265 (canonical-prefix violation). Those cost a full wrap-build round-trip to discover. This skill surfaces them in seconds, on disk.

It is read-only on the bundle — it unzips to a temp dir for inspection and never mutates the .ppmplugin. Fixes route back upstream (/generate-ppmplugin-manifest for manifest issues, the build skills for binary issues), then re-assemble + re-audit.

Read shared/ppmplugin-format.md §1 (layout), §3 (canonical-prefix), §4 (validator rules), §5 (Android DEX requirements) — this skill enforces all four against the built artifact.

What this skill does NOT do

  • Does not build, zip, or author anything — it inspects a finished .ppmplugin. Fixes are made upstream and re-assembled.
  • Does not patch the zip in place — a bundle is an immutable deliverable; mutating it would desync it from the staged sources. It points at the upstream skill instead.
  • Does not upload to Dataverse / wire into a canvas app (Stage 3 — deferred). "READY TO UPLOAD" means passes local verification, not uploaded.

Step 1 — Read shared docs + resolve the artifact + prereqs

  1. Read shared/shared-instructions.md and shared/ppmplugin-format.md.
  2. Resolve the bundle to audit:
    • If the user passed a path, use it. If it's a directory, look for <dir>/*.ppmplugin (prompt if multiple).
    • Else default to the assemble output: the single ppmplugin/<name>.ppmplugin at the repo root (the most-recently-built if several).
    • If none found, STOP with NEEDS_CONTEXT: no .ppmplugin to audit — run /assemble-ppmplugin first, or pass a path.
  3. Prereq block (shared-instructions §9.2). Policy: resolve, don't punt (§1.5) — locate a tool by path before failing:
CheckVerifyIf missing
jar (JDK)jar --version — used to list + extract the bundleSTOP BLOCKED: jar not found — install a JDK (JDK 17)
DEX scannerdexdump (preferred — locate in Android SDK build-tools/*/dexdump like /build-android-binary resolves $D8); else strings (mac/linux ships it)If neither: the DEX string checks degrade to WARNING ("DEX scan skipped — no dexdump/strings"); structural checks (magic, size) still run. On Windows without either, note the gap.
  1. Extract the bundle to a fresh temp dir for inspection (read-only on the original):
    work=$(mktemp -d); ( cd "$work" && jar xf "<abs path to .ppmplugin>" )
    
    $work = New-Item -ItemType Directory -Force (Join-Path $env:TEMP "ppm-audit"); Push-Location $work; jar xf "<abs path>"; Pop-Location
    

Each subsequent step appends findings to a running list as [SEVERITY] <check-id>: <result>. Don't stop at the first CRITICAL — collect everything so the user fixes in one pass. Severities: CRITICAL (will fail at upload or on device — blocks), WARNING (likely-wrong, may still work), INFO (style / advisory).


Step 2 — Category A: Zip structure

Listing comes from jar tf "<.ppmplugin>". The bundle is native-only (format §1): manifest.json at root + only the declared android/ / ios/ slices.

CheckAssertsSeverity
zip-manifest-at-rootmanifest.json is at the archive rootCRITICAL
zip-only-native-slicestop-level entries ⊆ { manifest.json, android/, ios/ }CRITICAL
zip-no-ts-js-layerNO src/, *.ts, *.tsx, *.js, extension.js, extension.hbc anywhere — the bundle ships no TS/JS layerCRITICAL
zip-no-build-dirsNO android-build/, ios-build/, node_modules/, dist/, build/ leaked inCRITICAL
zip-no-stray-filesNO META-INF/, .DS_Store, dotfiles, nested *.zip/*.ppmpluginWARNING
zip-reasonable-sizebundle < 50 MB (a larger one usually means React or node_modules got swept in)WARNING

A src/ tree, an extension.js/.hbc, or any *.ts is SDK-era / JS-layer leakage — flag CRITICAL and point at format §6 (the bundle is native binaries only).


Step 3 — Category B: Manifest schema, validator rules + field leakage

Read the extracted manifest.json. Re-run every ppmplugin-format §4 rule against the built artifact (defense in depth — the manifest may have been hand-edited after /generate-ppmplugin-manifest):

CheckAssertsSeverity
mf-name-shapename matches ^[a-z0-9][a-z0-9-]{0,63}$CRITICAL
mf-canonical-prefixeach receivers[].nativeModule starts with the canonical prefix of name (split on -/_, PascalCase each segment, join — §3) (Ordinal, case-sensitive)CRITICAL
mf-reserved-prefixno nativeModule starts with a reserved prefix (case-insensitive list in §4: Microsoft, MS, Intune, Wrap, Pcf, PowerApps, …)CRITICAL
mf-reserved-exact-knownno nativeModule matches the locally checked incompatible-name subset (§4: DeviceInfo, AuthenticationHelper, NetworkClient, DataverseOfflineProvider, IntuneMAM). Non-exhaustive; fix = rename getName() to a non-reserved form (add Module suffix or a vendor prefix) and re-deriveCRITICAL
mf-nativeModule-generic-nounnativeModule does NOT match the generic-platform-noun heuristic ^(Device|Network|File|Audio|Camera|Sensor|Location|Storage|Notification|Bluetooth|Wifi|Media|Photo|Contact|Calendar|Battery) — these bare names are both denylist-prone and collision-prone in the shared NativeModules namespace. (Heuristic, not the real list — DeviceInfo returned READY locally yet was server-rejected; this would have warned.)WARNING
mf-methodsevery receivers[].methods is present, non-empty, ≤32, each matches ^[a-zA-Z_$][a-zA-Z0-9_$]{0,127}$CRITICAL
mf-receiver-nameeach receivers[].name matches the JS-identifier regexCRITICAL
mf-version-presentversion is a non-empty stringWARNING
mf-no-sdk-fieldsNO entrypoints.js, entrypoints.ts, extension.js, extension.hbc, extensionClassName, jsLayer fields in the manifest. Our native-only policy, NOT a server gate — the wrap injector treats entrypoints.js as optional and accepts it; we flag it so the native-only bundle stays unambiguous. WARNING, not a block.WARNING

Note in the report that only a known subset of incompatible exact names is checked locally (mf-reserved-exact-known), so a clean local result does not guarantee the upload service will accept the name.


Step 4 — Category C: Manifest ↔ bundle consistency

The declared entrypoints must match the binaries actually in the zip — exactly. (/assemble-ppmplugin reconciles this at build time; auditing the finished bundle catches a hand-edited manifest or a hand-zipped bundle.)

CheckAssertsSeverity
consistency-has-nativeat least one of entrypoints.android / entrypoints.ios is declared (a manifest-only bundle has nothing to load)CRITICAL
consistency-android-dexif entrypoints.android declared → android/<entrypoints.android.dex> exists in the zipCRITICAL
consistency-ios-frameworkif entrypoints.ios declared → ios/<entrypoints.ios.framework>.framework/ exists in the zip (flat framework — see ios-flat-framework-not-xcframework in Category E)CRITICAL
consistency-no-orphanevery android/ or ios/ slice in the zip is backed by a matching declared entrypoint (no undeclared binary the runtime won't route)WARNING
consistency-packageClass-shapeentrypoints.android.packageClass is a fully-qualified Java class name (^([a-z][a-z0-9_]*\.)+[A-Z][A-Za-z0-9_]*$)CRITICAL
dispatch-composite-keythe composite routing key <name>/<receivers[].name> is well-formed (both segments present, JS-identifier-safe suffix) — this is what a dispatcher PCF binds as ReceiverKey to reach NativeModules.<nativeModule>.<method> (format §2 — Runtime dispatch contract)WARNING

Step 5 — Category D: DEX integrity + SDK-leakage byte-scan (Android)

Only if entrypoints.android is declared. The DEX is loaded via DexClassLoader (format §5); these confirm it's loadable and free of SDK-era leakage. Use dexdump -l plain <dex> if available, else strings <dex> — both expose the string/type pool the checks scan.

CheckAssertsSeverity
dex-magicfirst bytes are dex\n03[5-9] (valid DEX)CRITICAL
dex-has-packageClassthe entrypoints.android.packageClass type descriptor (Lcom/.../<Pascal>Package;) is in the DEX — otherwise runtime class loading failsCRITICAL
dex-has-module-classthe module class descriptor is in the DEXCRITICAL
dex-no-INativeExtensionNO INativeExtension, INativeOperation, INativeExtensionContext strings — SDK-era symbols in a native-only bundle. Cleanliness policy, not a server gate: the player ignores them (an old-pattern plugin still dispatches if its receivers[] map to real native modules), so surface it but don't block.WARNING
dex-no-jslayer-loaderNO HermesBytecodeLoader, WrapPluginJsLayerLoader strings — a JS-layer loader has no place in a native-only DEX (same: flagged, not blocking).WARNING
dex-no-sendAsyncNO sendAsync symbol — possible SDK transport leakWARNING

These SDK-leakage checks are the heart of the native-only contract: a clean compile can still pull these symbols in transitively. Scanning the built DEX is the only place they're catchable.


Step 6 — Category E: iOS framework structure (the wrap-CI gates)

Only if entrypoints.ios is declared. The CRITICAL set is exactly what the runtime's dlopen loading and wrap signing pipeline require. The umbrella header + module map are build hygiene only (the loader dlopens the binary; it never imports the module), so they're INFO — do not block a valid bundle on their absence.

CheckAssertsSeverity
ios-flat-framework-not-xcframeworkthe bundle ships ios/<framework>.framework/ — NOT ios/<framework>.xcframework/. If an .xcframework is present, FAIL with the wrap-CI error verbatim ("Framework '<Name>.framework' not found in plugin") and the fix: rebuild with /build-ios-binary (device slice, flat framework — drop -create-xcframework).CRITICAL
ios-binary-presentios/<framework>.framework/<framework> (the Mach-O binary, named exactly <framework>) exists — the loader dlopens Frameworks/<framework>.framework/<framework>, so a missing or differently-named binary won't loadCRITICAL
ios-framework-has-infoplistios/<framework>.framework/Info.plist exists with CFBundlePackageType=FMWK, CFBundleExecutable=<framework>, CFBundleIdentifier, CFBundleShortVersionString, MinimumOSVersion — a framework bundle with no Info.plist is invalid and the wrap codesign step rejects itCRITICAL
ios-framework-has-umbrellaios/<framework>.framework/Headers/<framework>.h umbrella header present — build hygiene only; the dlopen loader does not import the module, so its absence does NOT break loadingINFO
ios-framework-has-modulemapios/<framework>.framework/Modules/module.modulemap present — build hygiene only (same reason); not runtime-requiredINFO
ios-moduleClass-matchentrypoints.ios.moduleClass is the documented Obj-C class (RCT<Pascal>Module) and, when source is reachable, the .h/.m class name matches itWARNING
ios-moduleName-matcheswhen source is reachable, the .m declares + (NSString *)moduleName returning receivers[].nativeModule and does not use RCT_EXPORT_MODULE(...) (wrap plugins are registered by the runtime, not +load)CRITICAL

(Deep Mach-O introspection — Dynamic-vs-Static, dead-code-stripping, weak React link — is owned by /build-ios-binary at build time; here we verify the shipped framework is what the loader + wrap CI ingest.)


Step 7 — Category F: Source-to-receiver contract (when the repo is reachable)

These need the extension source. If the audit runs in the control repo (the bundle's source tree is present), run them; else mark each SKIPPED — source not reachable and note that the build skills already asserted them at build time.

CheckAssertsSeverity
src-getName-matchesthe Kotlin module's getName() literal == receivers[].nativeModule — mismatch = native module 'X' not loaded on device (compiles fine; hardest to catch)CRITICAL
src-methods-annotatedevery receivers[].methods[] entry has a matching @ReactMethod (Android) / RCT_EXPORT_METHOD (iOS)CRITICAL
src-package-fqn-matchesthe ReactPackage FQN == entrypoints.android.packageClassCRITICAL
src-package-no-arg-ctorthe <Pascal>Package (Android) has a public no-arg constructor — the runtime uses getDeclaredConstructor().newInstance(); a primary-constructor parameter list (class <Pascal>Package(...)) makes the plugin silently fail to load (Loaded 0 plugin package(s)). iOS analogue: the module class instantiates via [cls new] (no custom arg-ed initializer).CRITICAL
src-ios-module-no-arg-init(iOS) the module class has no arg-ed-only initializer — the player does NSClassFromString(moduleClass) → [cls new], so a class whose only initializer takes arguments won't instantiate and is skipped at load (§5b).CRITICAL
src-ios-requires-main-queue(iOS) if + (BOOL)requiresMainQueueSetup is declared, it returns NO (returning YES forces main-thread setup at launch and, with any heavy/throwing init, stalls or crashes startup — §5b).WARNING
src-method-single-map-parameach @ReactMethod / RCT_EXPORT_METHOD takes one ReadableMap/NSDictionary request param (then the Promise/resolver) — the wrap proxy spreads the PCF's args:[request] positionally, so a method with multiple positional params won't receive its payload (a no-arg op taking only the Promise is fine).WARNING
pcf-composite-key-matches-receiverif a sibling PCF (pcf/<…>/index.ts) declares COMPOSITE_KEY = "<name>/<receiver>" (or binds a ReceiverKey), it MUST equal <manifest.name>/<receivers[].name> — a mismatch means the PCF dispatches to a receiver the manifest never registers, failing on first call (real bug: PCF→Snapshot, manifest→DeviceInfoExtension).WARNING
pcf-uses-sendasyncif a sibling PCF (pcf/<…>/index.ts) is present, it MUST dispatch via window.PowerApps.NativeExtension.sendAsync(...) and MUST NOT call cordova.exec (or any cordova.*) directly. A direct cordova.exec call is the CRITICAL defect: the raw cordova global is not exposed to the PCF sandbox, so the call is a silent no-op on device (worst on Android) — it passes zip/manifest/DEX/key checks and only fails on-device. Fix: route through the host-injected sendAsync global (format §2).CRITICAL
pcf-envelope-is-raw-objectif a sibling PCF is present, the sendAsync payload MUST be a raw { method, args: [...] } object — the PCF must NOT pre-JSON.stringify it (sendAsync stringifies internally). A sendAsync(key, JSON.stringify({...})) double-encodes → the proxy JSON.parse yields a string, not {method,args} → BRIDGE_FAILED. Fix: pass the object literal, not a stringified one.CRITICAL
pcf-args-is-arrayif a sibling PCF is present, the inner args inside the sendAsync payload is a JSON array (args: [request]), not a bare object — after parsing, the wrap proxy does Array.isArray(parsed.args) ? parsed.args : [] and drops a non-array, so the native method gets no payload (format §2).WARNING
pcf-has-ambient-dtsif a sibling PCF is present, it ships a local PowerAppsNativeExtension.d.ts ambient declaration (typing window.PowerApps.NativeExtension.sendAsync) rather than importing a host SDK package — keeps the PCF host-agnostic.INFO
src-no-ReactModule-annotationthe module has no @ReactModule annotation (incompatible with DexClassLoader — format §5)CRITICAL
src-ctor-no-throwable-sideeffectsScope the scan to the module's construction closure, not the whole file — the primary/secondary constructor(s), the Kotlin init{} block(s), property initializers that run at construction (private val x = …), PLUS any private function they call (follow one level of foo() / this.foo()). Within that closure, flag: (a) register*Callback(…, null) or a bare Handler() / Handler(...) with no explicit Looper; (b) any side-effecting call (register*/add*Listener/observe/getSystemService+use/file or network I/O/runBlocking) not inside a try { } catch. @ReactMethod bodies are OUT of scope (they run per-call, not at construction). The module is constructed eagerly at bridge startup on a possibly Looper-less thread — an uncaught throw crashes the host at launch, before any UI (format §5). Fix: defer to lazy first-call init, pass Handler(Looper.getMainLooper()), wrap unavoidable init in try/catch. iOS analogue: apply the same scan to a throwing/heavy -init (instantiated eagerly via [cls new], §5b). Heuristic — the definitive catch is /test-native-extension Layer 1's load/init readiness asserts (static) + Layer 5's runtime launch crash-scan.WARNING
pcf-response-unwraps-messageif a sibling PCF is present, its sendAsync success path unwraps the wrap response container ({isUpdate, message:"<json>"}) — i.e. calls an extractResponse-style helper (parse result.data + probe message), not a bare single JSON.parse. A single-parse-only handler lands on the container and fails every call with UNEXPECTED_PAYLOAD though native succeeded (format §2).WARNING
src-promise-always-settledeach @ReactMethod / RCT_EXPORT_METHOD that takes a Promise / resolver+rejecter contains at least one promise.resolve / promise.reject (or resolve(...) / reject(...)) on a reachable path. A Promise-taking method that can return without settling leaves the maker with a hung control and no code/message.WARNING
src-listener-releasedeach register* / add*Listener / observe / acquired manager in the module has a matching release (unregister* / remove*Listener / .close() / .release()) in invalidate() / onCatalystInstanceDestroy() / teardown. A registered callback with no release leaks and can fire into a dead module.WARNING

Step 8 — Report + route fixes

Print the verdict as a visible fenced block (shared-instructions §9.1). Prefix each severity per shared-instructions.md §9.3 — 🔴 CRITICAL, 🟡 WARNING, 🔵 INFO, 🟢 READY — keeping the word so meaning survives a no-emoji terminal:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 audit-ppmplugin — pen-input.ppmplugin
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Path: ppmplugin/pen-input.ppmplugin   (v0.1.4, Android-only, 3.4 KB)
 🔴 CRITICAL: 0   🟡 WARNING: 1   🔵 INFO: 0   SKIPPED: 4 (source not reachable)

 🟡 [WARNING] zip-no-stray-files: .DS_Store present in android/
   Fix: re-run /assemble-ppmplugin (it excludes dotfiles).

 ⓘ The upload service may apply additional name checks not represented locally.
 🟢 ✓ READY TO UPLOAD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

(When there are CRITICALs, the verdict line reads 🔴 and the last line is 🔴 ✗ NOT READY — fix the CRITICAL findings above.)

Each non-clean finding carries Fix: with the concrete next action, routed to the owning skill (never a zip patch):

  • Manifest rule / leakage field → "edit + re-run /generate-ppmplugin-manifest, then /assemble-ppmplugin."
  • DEX leakage / missing class → "the Android source pulled in SDK symbols — strip the import; re-run /build-android-binary then /assemble-ppmplugin."
  • Zip stray-file / structure → "re-run /assemble-ppmplugin."

Then surface next steps via AskUserQuestion (shared-instructions §9.1):

  • Clean (0 CRITICAL): offer Re-run /generate-ppmplugin-manifest (only if a WARNING points there) / Stay — I'll upload manually (Stage 3 is not yet a skill). When the user picks a Run /… option, invoke it via the Skill tool in the same turn (§8 + §9.1 "execute, don't describe").
  • Dirty (≥1 CRITICAL): offer to jump to the owning skill of the first CRITICAL — Run /generate-ppmplugin-manifest or Run /build-android-binary — then re-assemble + re-audit. Invoke on selection.

Update the ## ppmplugin (third-party controls) block in .extension-state.md: Audit: <name>.ppmplugin <ISO timestamp> — <CRITICAL>C/<WARNING>W (READY | BLOCKED).

Return code:

  • DONE — 0 CRITICAL (clean, or warnings only ⇒ note them). The bundle passes local verification.
  • BLOCKED: audit found <N> CRITICAL — see findings — refuse the READY verdict; do not claim it's uploadable.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Activates and provisions a Power Pages website in a Power Platform environment via the Power Platform REST API. Use when the user wants to activate, provision, turn on, or enable a Power Pages website or portal.

The English summary is being prepared. Showing the original description.

microsoft/power-platform-skills1KUpdated Oct 20, 2026

Integrates Power Pages generative-AI summarization APIs (PREVIEW) into a Single Page Application (SPA) site — the Search Summary API and the Data Summarization API — on any record-detail or list page. Generates per-target service code (CSRF-handled) and AI site settings; delegates Web API settings, table permissions, and web roles to `/integrate-webapi` and `/create-webroles`. Use whenever a user wants AI/Copilot output that condenses Dataverse content on a Power Pages site — an AI summary, AI-generated overview or "key insights" across a record or list, a search-results summary, a case/incident summary, or recommendation-chip refinement — even when phrased as "AI-generated paragraph", "insights", or "overview". Do NOT use for: generative pages in model-driven apps (use the model-apps `genpage` skill), Copilot Studio agents/chatbots, summarizing documents or PDFs, Power BI dashboards, plain keyword search with no AI summary, or plain Dataverse CRUD (use `/integrate-webapi`).

The English summary is being prepared. Showing the original description.

microsoft/power-platform-skills1KUpdated Oct 20, 2026

Adds Azure DevOps connector to a Power Apps code app. Use when querying work items, creating bugs, managing pipelines, or making ADO API calls.

The English summary is being prepared. Showing the original description.

microsoft/power-platform-skills1KUpdated Oct 20, 2026

Internal implementation skill invoked by /add-native for camera, image picker, barcode scanner, QR scanner, and camera/gallery Dataverse artifact workflows.

The English summary is being prepared. Showing the original description.

microsoft/power-platform-skills1KUpdated Oct 20, 2026

Integrates Power Automate cloud flows into a Power Pages site. Lists available flows, suggests relevant ones based on intent, identifies scenarios and web roles, creates metadata files, and generates client-side code to call flows. Handles both new flow registration and adding already-registered flows to additional pages without re-creating metadata. Use when the user wants to add, connect, register, or link a Power Automate cloud flow to their site.

The English summary is being prepared. Showing the original description.

microsoft/power-platform-skills1KUpdated Oct 20, 2026

Adds any Power Platform connector to a Power Apps code app. Generic fallback for connectors not covered by a specific skill.

The English summary is being prepared. Showing the original description.

microsoft/power-platform-skills1KUpdated Oct 20, 2026

See all skills by microsoft

Report a problem with this skill