Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

AIOX Plugins — the public plugin catalog

This repository is the public, versioned index of plugins installable by the AIOX Cockpit (Ajuda ▾ → Plugins). It is deliberately separate from the product repository — the product repo is never referenced here in a way that would grant read access to its private content — so that publishing a plugin never exposes product source, and installing a plugin never depends on the product repository continuing to exist.

Decision record: ADR-COCKPIT-ENTERPRISE-PREMIUM-PACK, decisions D15–D24 (catalog: D22; namespace

  • declared shadowing: D23; the three no-going-back invariants: D24). This repo implements the catalog side of those decisions; the Cockpit-side consumer lives in the product repo.

What lives here

PathWhatStatus
schema/index-entry.schema.jsonThe versioned schema every index entry MUST conform to (AC2)Enforced by CI (structural check)
index/index.jsonThe production index. Ships empty — see "Why the production index is empty" belowNo real entries yet
fixtures/A non-production index + artifact used only to prove the publish pipeline works end-to-endTest data, never consumed by a real client
publisher/The publish pipeline (publish.mjs) and the despublish pipeline (retire.mjs) — both AIOX-operated services, not a human PR workflow (AC5, D22)Full D24 invariant suite wired in (055.W3.3)
lib/entry-schema.mjsShared validation (shape, artifact-identity binding, artifact-host allowlist, id-immutability, burned-name, tier-vocabulary) — imported by BOTH publisher/publish.mjs and scripts/validate-index.mjs/scripts/check-ledger-consistency.mjsEnforced publish-time AND in CI
lib/license-check.mjsOpens the artifact tarball and verifies a license file at the package root (D24(c))Enforced publish-time
lib/ledger.mjsThe persistent, append-only registry of every plugin_id ever published (ledger/plugin-ids.json) — survives an index entry's removal (VC-1)
ledger/plugin-ids.jsonThe ledger itself. Ships with zero real plugins, matching the production indexAppend-only, proved across its entire git history
scripts/check-ledger-append-only.mjsCI proof that the ledger's git history never removed/mutated an existing recordRun on every push
scripts/check-ledger-consistency.mjsCI structural re-check of id-immutability/burned-name against index/index.json, independent of publish-timeRun on every push
test/Automated unit tests (node:test, zero dependency) for lib/, the publish.mjs/retire.mjs CLIs, render-catalog.mjs, and the ledger checksRun on every push (ci.yml)
docs/CATALOG-AND-MIRROR.mdHow the index, the R2 artifact mirror, and the publish pipeline fit together
docs/SCHEMA.mdField-by-field explanation of the index entry schema
docs/INVARIANTS.mdThe four no-going-back invariants (D24 a/b/c + D21's AC8), how each is verified, and the explicitly-named design boundaries
lib/secret-rules.mjsThe vendored gitleaks rule corpus (MIT, a dated snapshot of a named upstream ref) — 14 rules across 14 covered classes, plus what was deliberately left out and whyEnforced publish-time AND in CI (055.W4.1)
lib/secret-scanner.mjsThe engine that runs those rules over the manifest and the artifact's real bytes, with its blind spots attached to every reportEnforced publish-time AND in CI
lib/pin.mjsVersion pin resolution (<plugin_id>@<version> → digest) — a pure function of (index, pin), which is what makes it deterministic and channel-independent at once
scripts/check-channel-separation.mjsCI proof that no executable file here reads binary-channel state (D19 / AC5)Run on every push
docs/SECRET-SCANNING.mdWhat the blocking scanner catches, why the rules are vendored rather than depended on, and — the important part — what it does not see
docs/PIN-AND-CHANNEL.mdThe pin, its determinism proof, its cost, and the plugin channel's independence from the binary channel

Blocking secret scanning (055.W4.1, D20(1))

A package containing a recognisable credential does not publish — failure, not warning, with no flag or environment variable that disables it. The scan covers the manifest (which becomes a public catalog entry) and the artifact's real bytes (what a client downloads and runs), using a vendored subset of gitleaks' rule corpus so the detection patterns are reused rather than reinvented.

node scripts/scan-secrets.mjs --artifact <plugin.tar.gz>
node scripts/scan-secrets.mjs --manifest <manifest.json> --json

Read docs/SECRET-SCANNING.md §5 before treating a clean scan as a safety verdict. In particular: the scan inspects the published manifest and artifact, not the target of an MCP pointer resolved at runtime ({command, args}, typically npx <package>), and an obfuscated or encoded secret escapes it entirely. Those limits are printed on every run, including successful ones.

A member the scan cannot read — binary, oversized, a duplicate/shadowed path, or a non-regular member — blocks the publish rather than being skipped: unscannable is treated as not publishable (§5.1), and the inventory comes from the archive's own member table, not from what survives extraction (§5.2).

Version pin + the plugin channel (055.W4.1, D20(2) / D19)

<plugin_id>@<version> resolves to a digest; the mirror path is content-addressed, so the same pin yields the same bytes. The plugin's update cycle is independent of the cockpit binary's (ADR-COCKPIT-UPDATE-CHANNELS, epic 017 — reused as a concept, never reimplemented here), and CI enforces that no file in this repo reads binary-channel state.

node scripts/resolve-pin.mjs --index index/index.json --pin sinkra-os@1.2.0 [--verify ./downloaded.tar.gz]

The pin is not pure gain. It freezes an install — which also means an already-installed artifact cannot be repaired by a later corrected build. Index freshness (055.W5.1, D20(5)) is what gives that capability back. The cost is carried on every resolution and printed in every output mode; the reasoning is in docs/PIN-AND-CHANNEL.md §2.

Capability analysis + mandatory allowed-tools (055.W4.2)

Every publishable skill MUST declare allowed-tools (kebab-case; comma/space string or YAML list). Publishing without it fails, unconditionally. Capabilities shown to the user are DERIVED by AIOX-side static analysis from the artifact's bytes — the publisher has no field in which to declare them, and a manifest that tries is refused.

This delivers visibility, not containment: nothing here sandboxes a plugin. v1 warns and displays; the blocking path exists in code, off by configuration, with the documented trigger "when opening to externals".

node scripts/analyze-capabilities.mjs --artifact <plugin.tar.gz> --require-allowed-tools
node scripts/analyze-capabilities.mjs --dir <skills-dir> --json

Full design reasoning, the two signals, and what the analysis cannot see: docs/CAPABILITIES.md.

Testing

node --test test/*.test.mjs

Node's built-in test runner — zero dependency added to a scaffolding repo. Covers: every refusal path in lib/entry-schema.mjs (artifact-identity binding, artifact-host allowlist, the D24 duplicate/immutability guard, shape validation), escapeMd's HTML/Markdown/injection neutralization plus AC9 legibility for benign input, renderCatalog's positive/negative shadow-warning cases, the publish.mjs CLI end to end (always with --no-push — no test ever runs git commit/git push), validate-index.mjs's CI-side gate, and a regression pin that index/index.json (production) stays empty (VC-5). Wired into .github/workflows/ci.yml, runs on every push and PR.

Why the production index is empty

Three invariants have to exist before the first real publication, because they are irreversible after it (D24): the plugin id becomes immutable (enforced against each entry's lineage_id, its stable identity across version bumps — see docs/INVARIANTS.md "Check (a)"), a retiredplugin_id is burned forever (never reused by any publisher, enforced by the persistent ledger — see docs/INVARIANTS.md "Check (b)"), and a license becomes mandatory at the package root. The same "before it exists, it costs nothing" logic is why the entry schema went 1.0.0 -> 2.0.0 inside this story: adding the required lineage_id was free while this index was still empty, and would have been a migration of an artifact every offline client pins by digest one publication later. A fourth, the publish-time half of D21 (AC8), was moved into the same story so a typo'd tier fails at publish, not on a paying client's machine. The CI that verifies all four now exists (055.W3.3 — see docs/INVARIANTS.md), but the first real publication is still a deliberate, separate authorization this story's own dispatch did not grant: index/index.json therefore continues to ship with zero entries; fixtures/index.json proves the pipeline against disposable data instead.

Identity: who can publish

The right to publish is itself an entitlement (D16). The identity recorded on every entry is the entitlement subject (publisher.subject) — never a GitHub handle — because it is the same mechanism that already proves payment and provisioning. See publisher/README.md for how the pipeline is invoked and what it assumes about that identity today. (publisher/retire.mjs, 055.W3.3, is the one deliberate exception to "nothing removes an entry" — see "Despublishing" below and docs/INVARIANTS.md; it does not act on identity, and it is distinct from the automatic index- freshness/expiry mechanism of D20(5), which still does not exist — see "What this repo does NOT do (yet)".)

Despublishing (publisher/retire.mjs, 055.W3.3)

retire.mjs removes a plugin_id's entry from a target index file AND, in the same operation, flips that plugin_id's record in ledger/plugin-ids.json to status: "retired" — see docs/INVARIANTS.md "Check (b)" for why both writes always happen together. This is a manual, explicit operation with a mandatory --reason; it is not the automatic index-freshness/expiry mechanism of D20(5) (still 055.W5.1, still does not exist) — retiring a plugin here is a deliberate act by whoever operates the pipeline, not something the system does on its own on a schedule or a version pin.

No PR flow, by design

Nothing in this repository is written by a human opening a pull request against it. The entry is written by an AIOX-operated service (publisher/publish.mjs) that commits and pushes directly. This is the other half of D22: the product repo's GitHub client (aiox-gh) is read-only by construction, and a publish-by-PR flow would have collided with that. Writing the catalog from a service, keyed by entitlement subject, dissolves the collision instead of reintroducing write access into a crate that is deliberately read-only.

Artifact mirror

Artifacts are mirrored in Cloudflare R2 infrastructure already operated by AIOX (no new infrastructure stood up for this repo) so that installing a plugin never depends on the author's own repository staying online. See docs/CATALOG-AND-MIRROR.md for the bucket, the path convention, and how a client is expected to verify what it downloads.

Repository hygiene — branch protection on main (fix-cycle-1)

main has branch protection (verify: gh api repos/SynkraAI/aiox-plugins/branches/main/protection). What is on, what is deliberately off, and why:

SettingValueWhy
allow_force_pushesoffThe commit history (every real publish) can't be rewritten.
allow_deletionsoffmain can't be deleted out from under installed clients.
required_linear_historyonNo merge commits muddying the index's history.
required_status_checkson, contexts: ["validate"], strictIf a PR is ever opened against this repo (e.g. by a human collaborator, since the publisher never opens one), CI must pass before it can merge.
required_pull_request_reviewsoff, deliberatelyTurning this on would require EVERY change — including publisher/publish.mjs's own direct commits — to go through a PR, which contradicts D22 (the publisher does not open a PR). This is the one setting NOT applied, and it's the reason the two facts ("branch is protected" and "the publisher pushes straight to main") are not in tension.
enforce_adminsoffConsistent with the row above — nothing here should silently start blocking the publisher.
restrictions (who may push)not set (any collaborator with write access)There is currently no distinct machine identity to name here — see the backlog card below.

What this protects against, concretely: nobody (accidentally or otherwise) rewrites or deletes the published history. What it does NOT protect against yet: a human collaborator with write access hand-editing index/index.json directly, bypassing publisher/publish.mjs's validation (schema, artifact-identity binding, D24 duplicate guard) entirely — CI would catch it, but only after the push lands, not as a merge gate, because there is no PR step to gate. Closing that gap needs a distinct service credential that restrictions can name instead of "anyone with write" — tracked, not fixed here, as a backlog card in the product repo (a founder-level infra/cost decision, not a catalog-repo code change; not linked from here, consistent with this repo never referencing the product repo, AC1).

What this repo does NOT do (yet)

  • It does not perform automatic pruning, removal, or revocation of a published entry based on freshness/staleness. That capability (index freshness + monotonic version, D20(5)) is scoped to a later story (055.W5.1) and does not exist here. What this repo DOES have, as of 055.W3.3, is publisher/retire.mjs — a manual, explicit despublish operation with a mandatory reason, needed to make check (b)'s "burned name" invariant provable at all (see "Despublishing" above and docs/INVARIANTS.md). Neither retirement nor anything else here acts based on entitlement/identity — see the "Identity" section above.
  • It does not sign the index. Signing (D20(3)) is a separate story (055.W4.3) with its own key material, deliberately kept in a vault separate from the entitlement signing key.
  • It does not run the secret-scanning / capability-analysis / version-pin checks of D20(1)(2)(4). Those are 055.W4.1/055.W4.2.

About

AIOX Cockpit plugin catalog — public index, separate from the product repo (D22)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages