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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/cli-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ Field notes:
store-disabled plugins absent from the runtime as intended;
`unexpectedlyEnabled` contains disabled plugins still active in that runtime.
`agentOnly` remains reserved for runtime plugins absent from the store.
Codex detailed queries additionally expose `runtimeMissing` and
`runtimeDisabled`, splitting the existing `missing` array without changing it.
Other agents may omit these optional fields.
Content drift is not represented; use `adg plugins sync` when a full runtime
refresh is required.

Expand Down
49 changes: 46 additions & 3 deletions docs/local-plugin-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ For project installs, the Claude entry uses `scope: "project"` and records a

## Other Runtimes

Codex reads the ADG marketplace export directly from `~/.agents/plugins`, but
`link --target codex` is still the supported way to regenerate projections and
ensure the runtime sees the latest store state.
Codex discovers ADG's marketplace, but loads plugins only after runtime
registration through its CLI. Use `link --target codex` for registration and
`sync --target codex` for a complete refresh.

Antigravity imports from ADG's generated `.antigravity-plugin/` projection.
Use `link --target antigravity` for first registration and `sync --target
Expand Down Expand Up @@ -142,3 +142,46 @@ runtime projections, and activates the plugin in each compatible runtime.
`unlink --target <runtime>` is different: it is a temporary per-runtime override
and does not change store state. A later `sync` restores the store's enabled or
disabled state. `remove` deletes the payload and catalog entry entirely.

## Codex Registration Recovery

`adg plugins status --target codex --global --json` preserves the existing
`missing` field and adds `runtimeMissing` (not installed) and `runtimeDisabled`
(installed but disabled). Queries do not rewrite the catalog or Codex config.
Query failures mean unknown state, not an empty installation.

Use `adg plugins enable <name> --global` to explicitly restore an enabled plugin,
including a missing registration or runtime disablement. Healthy registrations
are not reinstalled. `adg plugins update --global --repair` updates sources and
then repairs missing Codex registrations using the existing local snapshots;
it preserves runtime-disabled plugins even when their source changes. Enabled
plugins whose contents changed are refreshed through the same verified execution
plan. Ordinary `update` does not repair an
intentional `unlink`. `sync` explicitly reconciles the complete desired state.

Repair honors stored component selections and dependencies. Installation and
verification failures are reported individually; partial failure exits nonzero
without rolling back successful installations. Other runtimes retain their
existing behavior. CLI diagnostics are local output, not telemetry payloads.

### Official Account Switch Investigation

The relationship between official Codex account switching and registration
loss is unconfirmed. Capture scoped plugin status before switching, immediately
after switching, after restarting and opening a new conversation, and after
explicit repair. Compare only ADG plugin/marketplace settings and cache presence;
keep any config backup private and redact paths in shared evidence. Never collect
`auth.json`, credentials, account identifiers, or raw config in telemetry.
Registration recovery does not guarantee that an existing desktop conversation
refreshes its skills; verify discovery in a new conversation.

In repair mode, Codex state is observed before source updates. Other runtimes
retain their normal update behavior; Codex writes are deferred until a scoped
plan is ready. An unavailable snapshot allows source materialization but blocks
Codex installation, refresh, and cleanup, producing a nonzero result.

Dependencies are processed first. A dependency outside the selected scope is
checked, not automatically installed; unavailable or failed dependencies block
their dependents while independent plugins continue. Explicit `enable` includes
its dependency closure. Execution success and post-operation verification are
reported separately: an unverified installation is never displayed as enabled.
68 changes: 59 additions & 9 deletions src/agents/codex.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { SpanKind } from "@opentelemetry/api";
Expand Down Expand Up @@ -58,14 +58,26 @@ export function syncMarketplace(
marketplace: string,
runner: typeof run = run,
warn: (message: string) => void = console.warn,
): void {
): boolean {
const root = codexMarketplaceRoot(pluginsDir);
// Explicit stores need the same supported discovery layout as canonical stores.
if (root === pluginsDir && existsSync(marketplacePath(pluginsDir))) {
try {
const exportDir = join(root, ".agents", "plugins");
mkdirSync(exportDir, { recursive: true });
writeFileSync(join(exportDir, "marketplace.json"), readFileSync(marketplacePath(pluginsDir)));
} catch (error) {
warn(`failed to sync Codex marketplace (${marketplace}): ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
const add = runner(["plugin", "marketplace", "add", root]);
if (add.ok) return;
if (add.ok) return true;
const upgraded = runner(["plugin", "marketplace", "upgrade", marketplace]);
if (!upgraded.ok) {
warn(`failed to sync Codex marketplace (${marketplace}): ${upgraded.out.trim() || add.out.trim() || "codex plugin marketplace sync failed without an error message"}`);
}
return upgraded.ok;
Comment thread
RbBtSn0w marked this conversation as resolved.
}

/**
Expand Down Expand Up @@ -254,23 +266,30 @@ export const codexAgent: Agent = {
activate(ctx: AgentContext): AgentSyncResult {
const mp = writeCodexMarketplaceName(ctx.pluginsDir);
if (!available()) return skippedResult("codex");
syncMarketplace(ctx.pluginsDir, mp);
let diagnostic = "";
if (!syncMarketplace(ctx.pluginsDir, mp, run, (message) => { diagnostic = message; })) return { agent: "codex", affected: [], skipped: false, failures: [{ stage: "marketplace", errorType: "MARKETPLACE_SYNC_FAILED", message: diagnostic || "Codex marketplace registration failed; plugin installation was not attempted." }] };
const affected: string[] = [];
const failures: NonNullable<AgentSyncResult["failures"]> = [];
for (const p of ctx.plugins) {
if (run(["plugin", "add", `${p}@${mp}`]).ok) affected.push(p);
const result = run(["plugin", "add", `${p}@${mp}`]);
if (result.ok) affected.push(p);
else failures.push({ plugin: p, stage: "install", errorType: "CLI_FAILURE", message: result.out.trim() || "Codex installation failed." });
}
if (ctx.reconcileLegacyAliases && affected.length > 0) reconcileLegacyAliases(ctx.pluginsDir, mp, affected);
return { agent: "codex", affected, skipped: false };
return { agent: "codex", affected, skipped: false, ...(failures.length ? { failures } : {}) };
},

deactivate(ctx: AgentContext): AgentSyncResult {
const mp = writeCodexMarketplaceName(ctx.pluginsDir);
if (!available()) return skippedResult("codex");
const affected: string[] = [];
const failures: NonNullable<AgentSyncResult["failures"]> = [];
for (const p of ctx.plugins) {
if (run(["plugin", "remove", `${p}@${mp}`]).ok) affected.push(p);
const result = run(["plugin", "remove", `${p}@${mp}`]);
if (result.ok) affected.push(p);
else failures.push({ plugin: p, stage: "remove", errorType: "CLI_FAILURE", message: result.out.trim() || "Codex removal failed." });
}
return { agent: "codex", affected, skipped: false };
return { agent: "codex", affected, skipped: false, ...(failures.length ? { failures } : {}) };
},

// `codex plugin add` stages into `.remote-plugin-install-staging` and swaps the
Expand All @@ -281,12 +300,26 @@ export const codexAgent: Agent = {
return codexAgent.activate(ctx);
},

inspectPlugins(ctx) {
if (!available()) return undefined;
const marketplace = codexMarketplaceName(ctx.pluginsDir);
return getTracer().startActiveSpan("adg.codex.inspect_plugins", (span) => {
try {
const result = run(["plugin", "list", "--available", "--marketplace", marketplace, "--json"]);
if (!result.ok) return codexListFailure(result.out);
const records = parseCodexInspection(result.out, marketplace);
recordTelemetryEvent("adg.codex.inspect_plugins", { "inspection.valid": records !== undefined, "inspection.record_count": records?.length ?? 0 }, span);
return records ?? codexUnrecognizedListFailure(result.out);
} finally { span.end(); }
});
},

// Query Codex's live plugin state for `adg plugins status`, scoped to our
// generated marketplace. `available()` gates the query so an absent CLI is a
// quiet `undefined` ("unknown").
listInstalled(ctx: AgentContext): AgentListResult {
if (!available()) return undefined;
const mp = writeCodexMarketplaceName(ctx.pluginsDir);
const mp = codexMarketplaceName(ctx.pluginsDir);
if (!mp) return undefined; // no generated marketplace → can't scope the query
const jsonRes = run(["plugin", "list", "--json"]);
if (jsonRes.ok) {
Expand Down Expand Up @@ -372,3 +405,20 @@ export function parseCodexPluginList(out: string, marketplace: string): string[]
}
return names;
}

/** Reject malformed snapshots instead of interpreting them as missing registrations. */
export function parseCodexInspection(out: string, marketplace: string): import("./types.ts").AgentPluginRecord[] | undefined {
try {
const parsed = JSON.parse(out);
if (!Array.isArray(parsed?.installed) || !Array.isArray(parsed?.available)) return undefined;
const records = new Map<string, import("./types.ts").AgentPluginRecord>();
for (const record of [...parsed.installed, ...parsed.available]) {
if (!record || typeof record !== "object") return undefined;
if (record.marketplaceName !== marketplace) continue;
if (typeof record.name !== "string" || record.pluginId !== `${record.name}@${marketplace}` || typeof record.installed !== "boolean" || typeof record.enabled !== "boolean" || (record.enabled && !record.installed)) return undefined;
if (records.has(record.name)) return undefined;
records.set(record.name, { id: record.pluginId, name: record.name, installed: record.installed, enabled: record.enabled });
}
return [...records.values()];
} catch { return undefined; }
}
23 changes: 23 additions & 0 deletions src/agents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,29 @@ export interface AgentSyncResult {
affected: string[];
/** True when the agent's CLI wasn't present, so the op was skipped (never a hard failure). */
skipped: boolean;
failures?: AgentOperationFailure[];
/** Present only when the command performed post-operation verification. */
verified?: string[];
unverified?: string[];
preservedDisabled?: string[];
}

export interface AgentOperationFailure {
plugin?: string;
stage: "inspect" | "marketplace" | "install" | "remove" | "verify";
errorType: string;
message: string;
}

export interface AgentPluginRecord {
id: string;
name: string;
installed: boolean;
enabled: boolean;
}

export type AgentInspectionResult = AgentPluginRecord[] | AgentListFailure | undefined;

/** A live plugin query reached the agent CLI, but the CLI rejected the query. */
export interface AgentListFailure {
error: string;
Expand Down Expand Up @@ -91,6 +112,8 @@ export interface Agent {
* plugins simply omits it.
*/
listInstalled?(ctx: AgentContext): AgentListResult;
/** Detailed, read-only state; absence means this capability is unsupported. */
inspectPlugins?(ctx: AgentContext): AgentInspectionResult;
/**
* Best-effort removal of this agent's own registrations (marketplaces, cached
* snapshots, …) that still point at a plugin directory ADG no longer has on
Expand Down
Loading
Loading