@lesliechan721/aw 0.1.0-beta.54 → 0.1.0-beta.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/authoring/scripts/lib/plugin-version-check.mjs +1 -1
  2. package/dist/authoring/scripts/validate-plugin.mjs +208 -31
  3. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  4. package/dist/scripts/bundle-plugins.js +8 -3
  5. package/dist/scripts/bundle-plugins.js.map +1 -1
  6. package/dist/src/adapters/helpers.d.ts +7 -0
  7. package/dist/src/adapters/helpers.js.map +1 -1
  8. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  9. package/dist/src/commands/aw-capabilities.js +2 -0
  10. package/dist/src/commands/aw-capabilities.js.map +1 -1
  11. package/dist/src/commands/managed-commands.d.ts +26 -0
  12. package/dist/src/commands/managed-commands.js +277 -0
  13. package/dist/src/commands/managed-commands.js.map +1 -0
  14. package/dist/src/commands/operation-plan.d.ts +30 -1
  15. package/dist/src/commands/operation-plan.js.map +1 -1
  16. package/dist/src/commands/operation-planner.d.ts +1 -0
  17. package/dist/src/commands/operation-planner.js +1 -0
  18. package/dist/src/commands/operation-planner.js.map +1 -1
  19. package/dist/src/commands/plugin-capabilities.js +29 -1
  20. package/dist/src/commands/plugin-capabilities.js.map +1 -1
  21. package/dist/src/commands/rebuild-command.d.ts +2 -1
  22. package/dist/src/commands/rebuild-command.js +66 -26
  23. package/dist/src/commands/rebuild-command.js.map +1 -1
  24. package/dist/src/commands/rebuild-integration.d.ts +13 -1
  25. package/dist/src/commands/rebuild-integration.js +535 -61
  26. package/dist/src/commands/rebuild-integration.js.map +1 -1
  27. package/dist/src/commands/rebuild-operations.d.ts +3 -1
  28. package/dist/src/commands/rebuild-operations.js +9 -5
  29. package/dist/src/commands/rebuild-operations.js.map +1 -1
  30. package/dist/src/generators/local-plugin.js +16 -0
  31. package/dist/src/generators/local-plugin.js.map +1 -1
  32. package/dist/src/generators/permissions.d.ts +5 -2
  33. package/dist/src/generators/permissions.js +28 -15
  34. package/dist/src/generators/permissions.js.map +1 -1
  35. package/dist/src/index.js +11 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/output/command-receipt.d.ts +3 -1
  38. package/dist/src/output/command-receipt.js +30 -9
  39. package/dist/src/output/command-receipt.js.map +1 -1
  40. package/dist/src/output/rebuild-progress.js +14 -0
  41. package/dist/src/output/rebuild-progress.js.map +1 -1
  42. package/dist/src/portable/materializer.js +3 -0
  43. package/dist/src/portable/materializer.js.map +1 -1
  44. package/dist/src/review/skill-materialization.d.ts +14 -1
  45. package/dist/src/review/skill-materialization.js +138 -0
  46. package/dist/src/review/skill-materialization.js.map +1 -1
  47. package/dist/src/tui/console-app-v4.js +19 -7
  48. package/dist/src/tui/console-app-v4.js.map +1 -1
  49. package/dist/src/tui/controller-runtime.js +23 -9
  50. package/dist/src/tui/controller-runtime.js.map +1 -1
  51. package/dist/src/tui/controller.d.ts +5 -1
  52. package/dist/src/tui/controller.js +24 -3
  53. package/dist/src/tui/controller.js.map +1 -1
  54. package/dist/src/tui/diagnostic-projection.js +34 -6
  55. package/dist/src/tui/diagnostic-projection.js.map +1 -1
  56. package/dist/src/tui/operation-review-planner.js +102 -16
  57. package/dist/src/tui/operation-review-planner.js.map +1 -1
  58. package/dist/src/types.d.ts +22 -2
  59. package/dist/src/types.js.map +1 -1
  60. package/dist/src/utils/artifact-plan.d.ts +4 -3
  61. package/dist/src/utils/artifact-plan.js +50 -7
  62. package/dist/src/utils/artifact-plan.js.map +1 -1
  63. package/dist/src/utils/artifact-renderer.d.ts +7 -3
  64. package/dist/src/utils/artifact-renderer.js +19 -6
  65. package/dist/src/utils/artifact-renderer.js.map +1 -1
  66. package/dist/src/utils/artifact-request.d.ts +16 -5
  67. package/dist/src/utils/artifact-request.js +27 -15
  68. package/dist/src/utils/artifact-request.js.map +1 -1
  69. package/dist/src/utils/artifact-sources.d.ts +4 -0
  70. package/dist/src/utils/artifact-sources.js +17 -0
  71. package/dist/src/utils/artifact-sources.js.map +1 -1
  72. package/dist/src/utils/bundle-layout.js +7 -0
  73. package/dist/src/utils/bundle-layout.js.map +1 -1
  74. package/dist/src/utils/claude-skills-root.d.ts +8 -0
  75. package/dist/src/utils/claude-skills-root.js +76 -0
  76. package/dist/src/utils/claude-skills-root.js.map +1 -0
  77. package/dist/src/utils/codex-skills-root.d.ts +4 -0
  78. package/dist/src/utils/codex-skills-root.js +25 -0
  79. package/dist/src/utils/codex-skills-root.js.map +1 -0
  80. package/dist/src/utils/dynamic-skill-tree.d.ts +1 -0
  81. package/dist/src/utils/dynamic-skill-tree.js +25 -6
  82. package/dist/src/utils/dynamic-skill-tree.js.map +1 -1
  83. package/dist/src/utils/local-marketplace.d.ts +9 -0
  84. package/dist/src/utils/local-marketplace.js +170 -90
  85. package/dist/src/utils/local-marketplace.js.map +1 -1
  86. package/dist/src/utils/managed-command-closure.d.ts +3 -0
  87. package/dist/src/utils/managed-command-closure.js +35 -0
  88. package/dist/src/utils/managed-command-closure.js.map +1 -0
  89. package/dist/src/utils/managed-command-evidence.d.ts +7 -0
  90. package/dist/src/utils/managed-command-evidence.js +86 -0
  91. package/dist/src/utils/managed-command-evidence.js.map +1 -0
  92. package/dist/src/utils/managed-command-materialization.d.ts +106 -0
  93. package/dist/src/utils/managed-command-materialization.js +307 -0
  94. package/dist/src/utils/managed-command-materialization.js.map +1 -0
  95. package/dist/src/utils/managed-command-runtime.d.ts +32 -0
  96. package/dist/src/utils/managed-command-runtime.js +110 -0
  97. package/dist/src/utils/managed-command-runtime.js.map +1 -0
  98. package/dist/src/utils/managed-command-store.d.ts +124 -0
  99. package/dist/src/utils/managed-command-store.js +576 -0
  100. package/dist/src/utils/managed-command-store.js.map +1 -0
  101. package/dist/src/utils/ownership.d.ts +2 -0
  102. package/dist/src/utils/plugin-config-impact.d.ts +8 -1
  103. package/dist/src/utils/plugin-config-impact.js +101 -26
  104. package/dist/src/utils/plugin-config-impact.js.map +1 -1
  105. package/dist/src/utils/plugin-config-write.js +142 -60
  106. package/dist/src/utils/plugin-config-write.js.map +1 -1
  107. package/dist/src/utils/rendered-contract.js +18 -1
  108. package/dist/src/utils/rendered-contract.js.map +1 -1
  109. package/package.json +3 -3
@@ -1,7 +1,14 @@
1
1
  import crypto from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import fs from 'fs-extra';
4
+ import { catalogFileMode } from '@lesliechan721/aw-plugin-core';
5
+ import { COMMAND_PRODUCER_RECORD_FILE, getManagedCommandArtifactTargets, getCommandPackageId } from '@lesliechan721/aw-plugin-core/manifest';
6
+ import os from 'node:os';
7
+ import { commandSetsForProducer, createManagedCommandReferenceResolver, discoverRetainedManagedCommandPackages, retainedManagedCommandPackageResult, managedCommandProducerIdentity, prepareManagedCommandPackage, resolveSkillManagedCommandReferences, } from '../utils/managed-command-materialization.js';
8
+ import { assertManagedCommandBindingIdentities, normalizeInstalledCommandProducer, managedCommandDiagnostic, ManagedCommandError, publishManagedCommandBindings, revokeManagedCommandBindings, syncManagedCommandPackage, readInstalledCommandProducer, listManagedCommands } from '../utils/managed-command-store.js';
4
9
  import { injectInstalledRuntimeConfig, carryRuntimeConfigNativeReceipt } from '../utils/runtime-config.js';
10
+ import { resolveCodexSkillsRelativeRoot } from '../utils/codex-skills-root.js';
11
+ import { resolveClaudeSkillsRelativeRoot } from '../utils/claude-skills-root.js';
5
12
  import { generateAgentArtifacts } from '../generators/agents.js';
6
13
  import { generateInstructionArtifacts, renderCodexManagedInstructionBlock, } from '../generators/instructions.js';
7
14
  import { generateCodexPermissionArtifacts, mergeClaudePermissions, } from '../generators/permissions.js';
@@ -24,7 +31,7 @@ import { validateRuntimeSkillClosure } from '../utils/runtime-skill-closure.js';
24
31
  import { findSensitiveTokenPath } from '../utils/sensitive-content.js';
25
32
  import { atomicRemoveConfigFile, atomicReplaceConfigFile, withConfigFileMutation, } from '../utils/plugin-config.js';
26
33
  import { readCatalogSourcesConfigSnapshot } from '../utils/catalog-sources.js';
27
- import { assertSafeNamespacedMarketplacePluginRoot, getNamespacedMarketplaceName, MarketplaceRootValidationError, projectMarketplaceManifestForPlatform, readStrictNamespacedMarketplaceRoot, replayNamespacedMarketplacePluginProjections, replayNamespacedCodexSkillOverlays, } from '../utils/local-marketplace.js';
34
+ import { assertSafeNamespacedMarketplacePluginRoot, getNamespacedMarketplaceName, MarketplaceRootValidationError, projectMarketplaceManifestForPlatform, readStrictNamespacedMarketplaceRoot, replayNamespacedMarketplacePluginProjections, refreshPreservedPlatformReceipt, replayNamespacedCodexSkillOverlays, } from '../utils/local-marketplace.js';
28
35
  import { commitMarketplaceGenerationBackup } from '../utils/orphan-marketplace-cleanup.js';
29
36
  import { readClaudeSettings, writeClaudeSettings } from '../adapters/helpers.js';
30
37
  import { analyzeCodexSurface } from '../utils/codex-surface-analysis.js';
@@ -61,6 +68,155 @@ function withoutVerifiedRequirementsReceipt(plugin, files) {
61
68
  delete output[AW_PLUGIN_REQUIREMENTS_RECEIPT_PATH];
62
69
  return output;
63
70
  }
71
+ const commandPackagePreparationCaches = new WeakMap();
72
+ const staticCommandPreparationCaches = new WeakMap();
73
+ async function prepareStaticCommandArtifacts(options) {
74
+ const targets = getManagedCommandArtifactTargets(options.plugin.manifest).filter((target) => target.producer.kind === 'static-skill' && target.platform === options.platform);
75
+ if (!targets.length)
76
+ return [];
77
+ const config = await resolveArtifactRenderConfig({ sourceId: options.plugin.sourceId, sourceIdentity: options.plugin.sourceIdentity, logicalIdentity: options.plugin.catalogLogicalIdentity, pluginName: options.plugin.manifest.name, manifest: options.plugin.manifest, scope: 'user', cwd: options.cwd, env: options.env });
78
+ const key = JSON.stringify([options.plugin.rootDir, options.platform, config]);
79
+ let cache = staticCommandPreparationCaches.get(options.context);
80
+ if (!cache) {
81
+ cache = new Map();
82
+ staticCommandPreparationCaches.set(options.context, cache);
83
+ }
84
+ let prepared = cache.get(key);
85
+ if (!prepared) {
86
+ prepared = (async () => {
87
+ const rendered = await runArtifactRenderer({ plugin: options.plugin, event: 'install', platform: options.platform, scope: 'user', configScope: 'user', cwd: options.cwd, commandEnv: options.env, requestedArtifactIds: targets.map((target) => target.id) });
88
+ return validateArtifactPlan({ manifest: options.plugin.manifest, plan: rendered.plan, platform: options.platform, scope: 'user', requestedArtifactIds: rendered.context.requestedArtifactIds, renderConfig: rendered.context.config, rendererDigest: rendered.target.rendererDigest, manifestDigest: rendered.target.manifestDigest }).artifacts;
89
+ })();
90
+ cache.set(key, prepared);
91
+ }
92
+ return prepared;
93
+ }
94
+ /** Normal write preparation compares its real candidate; read-only inspection never calls this. */
95
+ export async function hasPreparedStaticNativeCommandDrift(options) {
96
+ const artifacts = await prepareStaticCommandArtifacts({ ...options, platform: 'codex' });
97
+ if (!artifacts.length)
98
+ return false;
99
+ const pluginRoot = path.join(options.marketplaceRoot, 'plugins', options.plugin.manifest.name);
100
+ const skillsRoot = path.join(pluginRoot, await resolveCodexSkillsRelativeRoot({ ...options.plugin, rootDir: pluginRoot }));
101
+ for (const set of options.plugin.manifest.managedCommands?.sets ?? []) {
102
+ if (set.producer.kind !== 'static-skill' || set.producer.platform !== 'codex' || set.mode !== 'dynamic')
103
+ continue;
104
+ const producer = await managedCommandProducerIdentity({ ...options, producer: set.producer, platform: 'codex', scope: 'user' });
105
+ const expected = commandSetsForProducer({ plugin: options.plugin, producer: set.producer, artifacts });
106
+ const previous = await readInstalledCommandProducer(path.join(skillsRoot, set.producer.skillKey)).catch(() => null);
107
+ if (!previous || JSON.stringify(previous.producer) !== JSON.stringify(producer) || JSON.stringify(previous.sets) !== JSON.stringify(expected))
108
+ return true;
109
+ }
110
+ return false;
111
+ }
112
+ async function commandPackageActions(options) {
113
+ const actions = [];
114
+ const candidates = [];
115
+ for (const declaration of options.plugin.manifest.managedCommands?.packages ?? []) {
116
+ if (!declaration.platforms.includes(options.platform))
117
+ continue;
118
+ const producer = await managedCommandProducerIdentity({ ...options, scope: declaration.scope, producer: { kind: 'package', packageKey: declaration.key } });
119
+ let cache = commandPackagePreparationCaches.get(options.context);
120
+ if (!cache) {
121
+ cache = new Map();
122
+ commandPackagePreparationCaches.set(options.context, cache);
123
+ }
124
+ let preparation = cache.get(getCommandPackageId(producer));
125
+ if (!preparation) {
126
+ preparation = (async () => {
127
+ const prepared = await prepareManagedCommandPackage({ ...options, packageKey: declaration.key, scope: declaration.scope });
128
+ try {
129
+ const files = [];
130
+ async function visit(directory, prefix) {
131
+ for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
132
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
133
+ const absolute = path.join(directory, entry.name);
134
+ if (entry.isDirectory())
135
+ await visit(absolute, relativePath);
136
+ else {
137
+ const content = await fs.readFile(absolute);
138
+ files.push({ relativePath, contentBase64: content.toString('base64'), contentDigest: `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`, mode: (await fs.stat(absolute)).mode & 0o777 });
139
+ }
140
+ }
141
+ }
142
+ await visit(prepared.resourceRoot, '');
143
+ files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
144
+ return { action: { kind: 'syncManagedCommandPackage', pluginId: pluginActionId(options.plugin), scope: declaration.scope, producer: prepared.producer, sets: prepared.sets, files, contentDigest: `sha256:${crypto.createHash('sha256').update(JSON.stringify(files)).digest('hex')}` }, candidate: { producer: prepared.producer, sets: prepared.sets } };
145
+ }
146
+ finally {
147
+ await prepared.cleanup();
148
+ }
149
+ })();
150
+ cache.set(getCommandPackageId(producer), preparation);
151
+ }
152
+ const prepared = await preparation;
153
+ actions.push(prepared.action);
154
+ candidates.push(prepared.candidate);
155
+ }
156
+ return { actions, candidates };
157
+ }
158
+ async function prepareSkillCommandCandidate(options) {
159
+ const sets = options.sets ?? commandSetsForProducer(options);
160
+ if (sets.length === 0)
161
+ return undefined;
162
+ const producer = await managedCommandProducerIdentity(options);
163
+ if (Object.keys(options.tree.files).length === 0)
164
+ return { producer, sets: sets.map((set) => ({ ...set, state: 'disabled', entries: [] })) };
165
+ if (Object.hasOwn(options.tree.files, COMMAND_PRODUCER_RECORD_FILE))
166
+ throw new Error('commandProducerReservedResource: plugin cannot provide the command producer record.');
167
+ for (const set of sets)
168
+ for (const entry of set.entries) {
169
+ if (!options.tree.files[entry.entry])
170
+ throw new Error(`commandResourceInvalid: command entry ${entry.entry} is missing from the final Skill.`);
171
+ if (process.platform !== 'win32' && entry.runner === 'executable' && options.tree.fileModes?.[entry.entry] !== 0o755)
172
+ throw new Error(`commandRunnerUnsupported: command entry ${entry.entry} has no executable source file.`);
173
+ }
174
+ options.tree.files[COMMAND_PRODUCER_RECORD_FILE] = Buffer.from(`${JSON.stringify({ schemaVersion: 1, producer, sets }, null, 2)}\n`);
175
+ return { producer, sets };
176
+ }
177
+ async function prepareStaticNativeSkillActions(options) {
178
+ if (!options.plugin.manifest.skills)
179
+ return { actions: [], candidates: [] };
180
+ if (!options.plugin.manifest.requires?.aw.capabilities?.includes('aw.managed-commands.v1'))
181
+ return { actions: [], candidates: [] };
182
+ const scope = 'user';
183
+ const root = path.join(options.plugin.rootDir, options.platform, options.plugin.manifest.skills.replace(/^\.\//u, ''));
184
+ const trees = [];
185
+ const candidates = [];
186
+ let artifacts = [...options.artifacts];
187
+ const missingTargets = getManagedCommandArtifactTargets(options.plugin.manifest).filter((target) => {
188
+ const producer = target.producer;
189
+ return producer.kind === 'static-skill' && target.platform === options.platform && !artifacts.some((artifact) => artifact.id === target.id) && !options.commandCandidates.some((candidate) => candidate.producer.kind === 'static-skill' && candidate.producer.key === producer.skillKey && candidate.producer.platform === options.platform && candidate.producer.scope === scope);
190
+ });
191
+ if (missingTargets.length) {
192
+ const rendered = await runArtifactRenderer({ plugin: options.plugin, event: 'install', platform: options.platform, scope, configScope: scope, cwd: options.cwd, commandEnv: options.env, requestedArtifactIds: missingTargets.map((target) => target.id) });
193
+ artifacts.push(...validateArtifactPlan({ manifest: options.plugin.manifest, plan: rendered.plan, platform: options.platform, scope, requestedArtifactIds: rendered.context.requestedArtifactIds, renderConfig: rendered.context.config, rendererDigest: rendered.target.rendererDigest, manifestDigest: rendered.target.manifestDigest }).artifacts);
194
+ }
195
+ for (const entry of await fs.readdir(root, { withFileTypes: true })) {
196
+ if (!entry.isDirectory())
197
+ continue;
198
+ const tree = await readDynamicSkillTree(path.join(root, entry.name), `${options.platform}.skill`);
199
+ if (!tree)
200
+ continue;
201
+ const previous = options.commandCandidates.find((candidate) => candidate.producer.kind === 'static-skill' && candidate.producer.key === tree.skillKey && candidate.producer.platform === options.platform && candidate.producer.scope === scope);
202
+ const candidate = await prepareSkillCommandCandidate({ ...options, scope, producer: { kind: 'static-skill', platform: options.platform, skillKey: tree.skillKey }, tree, artifacts, ...(previous ? { sets: previous.sets } : {}) });
203
+ if (candidate)
204
+ candidates.push(candidate);
205
+ trees.push(tree);
206
+ }
207
+ const resolver = await createManagedCommandReferenceResolver({ ...options, scope, candidates: [...options.commandCandidates, ...candidates] });
208
+ const actions = [];
209
+ for (const tree of trees) {
210
+ const hasCommandReferences = Object.entries(tree.files).some(([relative, content]) => relative.endsWith('.md') && content.includes('{{aw.command:'));
211
+ const hasCommandProducer = Object.hasOwn(tree.files, COMMAND_PRODUCER_RECORD_FILE);
212
+ if (!hasCommandReferences && !hasCommandProducer)
213
+ continue;
214
+ resolveSkillManagedCommandReferences(tree, resolver);
215
+ const files = Object.entries(tree.files).map(([relativePath, content]) => ({ relativePath, contentBase64: content.toString('base64'), contentDigest: `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`, ...(tree.fileModes?.[relativePath] ? { mode: tree.fileModes[relativePath] } : {}) })).sort((left, right) => left.relativePath.localeCompare(right.relativePath));
216
+ actions.push({ kind: 'materializeNativeSkill', pluginId: pluginActionId(options.plugin), platform: options.platform, scope, skillKey: tree.skillKey, files, contentDigest: `sha256:${crypto.createHash('sha256').update(JSON.stringify(files)).digest('hex')}` });
217
+ }
218
+ return { actions, candidates };
219
+ }
64
220
  export class CodexActivationPostcheckError extends Error {
65
221
  code = 'codexActivationPostcheckFailed';
66
222
  codexPluginIds;
@@ -356,6 +512,7 @@ function pristineTreeDescriptor(tree) {
356
512
  .map(([relativePath, content]) => ({
357
513
  relativePath,
358
514
  contentDigest: `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`,
515
+ ...(tree.fileModes?.[relativePath] ? { mode: tree.fileModes[relativePath] } : {}),
359
516
  }));
360
517
  }
361
518
  function pluginActionId(plugin) {
@@ -367,6 +524,7 @@ function skillWriteAction(options) {
367
524
  relativePath,
368
525
  contentBase64: content.toString('base64'),
369
526
  contentDigest: `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`,
527
+ ...(options.tree.fileModes?.[relativePath] ? { mode: options.tree.fileModes[relativePath] } : {}),
370
528
  })),
371
529
  {
372
530
  relativePath: '.agent-workflows-owner.json',
@@ -455,6 +613,9 @@ function mergePermissionStates(values) {
455
613
  allow: [...new Set(values.flatMap((value) => value?.allow ?? []))],
456
614
  ask: [...new Set(values.flatMap((value) => value?.ask ?? []))],
457
615
  deny: [...new Set(values.flatMap((value) => value?.deny ?? []))],
616
+ ...(values.some((value) => value?.excludedCommands !== undefined) ? {
617
+ excludedCommands: [...new Set(values.flatMap((value) => value?.excludedCommands ?? []))],
618
+ } : {}),
458
619
  };
459
620
  }
460
621
  function subtractPermissionState(value, generated) {
@@ -467,6 +628,9 @@ function subtractPermissionState(value, generated) {
467
628
  allow: (value?.allow ?? []).filter((entry) => !remove.allow.has(entry)),
468
629
  ask: (value?.ask ?? []).filter((entry) => !remove.ask.has(entry)),
469
630
  deny: (value?.deny ?? []).filter((entry) => !remove.deny.has(entry)),
631
+ ...(value?.excludedCommands ? {
632
+ excludedCommands: value.excludedCommands.filter((entry) => !generated.excludedCommands?.includes(entry)),
633
+ } : {}),
470
634
  };
471
635
  }
472
636
  function intersectPermissionState(value, generated) {
@@ -479,11 +643,17 @@ function intersectPermissionState(value, generated) {
479
643
  allow: (value?.allow ?? []).filter((entry) => keep.allow.has(entry)),
480
644
  ask: (value?.ask ?? []).filter((entry) => keep.ask.has(entry)),
481
645
  deny: (value?.deny ?? []).filter((entry) => keep.deny.has(entry)),
646
+ ...(value?.excludedCommands ? {
647
+ excludedCommands: value.excludedCommands.filter((entry) => generated.excludedCommands?.includes(entry)),
648
+ } : {}),
482
649
  };
483
650
  }
484
651
  function rebuildClaudePermissions(settings, ownerKey, desired) {
485
652
  const previousGenerated = mergePermissionStates(Object.values(settings.awGenerated?.permissions ?? {}));
486
- const currentPermissions = mergePermissionStates([settings.permissions]);
653
+ const currentPermissions = mergePermissionStates([{
654
+ ...settings.permissions,
655
+ ...(settings.sandbox?.excludedCommands ? { excludedCommands: settings.sandbox.excludedCommands } : {}),
656
+ }]);
487
657
  const manual = mergePermissionStates([
488
658
  subtractPermissionState(currentPermissions, previousGenerated),
489
659
  intersectPermissionState(intersectPermissionState(settings.awGenerated?.manualPermissions, previousGenerated), currentPermissions),
@@ -497,16 +667,22 @@ function rebuildClaudePermissions(settings, ownerKey, desired) {
497
667
  if (Object.keys(settings.awGenerated.permissions).length === 0) {
498
668
  delete settings.awGenerated.permissions;
499
669
  }
500
- if (manual.allow.length || manual.ask.length || manual.deny.length) {
670
+ if (manual.allow.length || manual.ask.length || manual.deny.length || manual.excludedCommands?.length) {
501
671
  settings.awGenerated.manualPermissions = manual;
502
672
  }
503
673
  else {
504
674
  delete settings.awGenerated.manualPermissions;
505
675
  }
506
- settings.permissions = mergePermissionStates([
676
+ const combined = mergePermissionStates([
507
677
  manual,
508
678
  ...Object.values(settings.awGenerated.permissions ?? {}),
509
679
  ]);
680
+ settings.permissions = {
681
+ ...settings.permissions, allow: combined.allow, ask: combined.ask, deny: combined.deny,
682
+ };
683
+ if (combined.excludedCommands !== undefined) {
684
+ settings.sandbox = { ...settings.sandbox, excludedCommands: combined.excludedCommands };
685
+ }
510
686
  if (settings.awGenerated.permissions === undefined
511
687
  && settings.awGenerated.manualPermissions === undefined
512
688
  && settings.awGenerated.hooks === undefined
@@ -946,14 +1122,17 @@ function renderCodexRole(options) {
946
1122
  ].join('\n');
947
1123
  }
948
1124
  async function resolveDynamicArtifacts(options) {
949
- const requestBatches = options.requestedArtifactIds
1125
+ const preparedArtifacts = options.platform === 'codex' && options.context
1126
+ ? await prepareStaticCommandArtifacts({ ...options, context: options.context }) : [];
1127
+ const preparedIds = new Set(preparedArtifacts.map((artifact) => artifact.id));
1128
+ const requestBatches = (options.requestedArtifactIds
950
1129
  ? [options.requestedArtifactIds]
951
1130
  : resolveAwDeliveredArtifactRequestBatches({
952
1131
  manifest: options.plugin.manifest,
953
1132
  platform: options.platform,
954
1133
  scope: options.scope,
955
- });
956
- if (requestBatches.length === 0) {
1134
+ })).map((batch) => batch.filter((id) => !preparedIds.has(id))).filter((batch) => batch.length > 0);
1135
+ if (requestBatches.length === 0 && preparedArtifacts.length === 0) {
957
1136
  return { artifacts: [], sourceDirs: () => [], sensitiveValues: [] };
958
1137
  }
959
1138
  const previousArtifacts = [...options.previousArtifacts];
@@ -981,7 +1160,7 @@ async function resolveDynamicArtifacts(options) {
981
1160
  cwd: options.cwd,
982
1161
  env: options.env,
983
1162
  });
984
- const artifacts = [];
1163
+ const artifacts = [...preparedArtifacts];
985
1164
  const sourceDirsByArtifact = new Map();
986
1165
  for (const render of renders) {
987
1166
  artifacts.push(...validateArtifactPlan({
@@ -1029,7 +1208,7 @@ function dynamicSkillWrite(options) {
1029
1208
  id: options.artifact.id,
1030
1209
  target: options.artifact.target,
1031
1210
  state: options.artifact.state,
1032
- ...(options.artifact.disabledReason
1211
+ ...('disabledReason' in options.artifact && options.artifact.disabledReason
1033
1212
  ? { disabledReason: options.artifact.disabledReason }
1034
1213
  : {}),
1035
1214
  refreshKeys: [...options.artifact.refreshKeys],
@@ -1053,28 +1232,6 @@ function dynamicSkillWrite(options) {
1053
1232
  } : {}),
1054
1233
  };
1055
1234
  }
1056
- async function resolveCodexSkillsRelativeRoot(plugin, marketplaceFallbackRoot) {
1057
- const manifestPaths = [
1058
- path.join(plugin.rootDir, '.codex-plugin', 'plugin.json'),
1059
- ...(marketplaceFallbackRoot
1060
- ? [path.join(marketplaceFallbackRoot, '.codex-plugin', 'plugin.json')]
1061
- : []),
1062
- ];
1063
- let manifest = null;
1064
- for (const manifestPath of manifestPaths) {
1065
- manifest = await fs.readJson(manifestPath).catch(() => null);
1066
- if (manifest)
1067
- break;
1068
- }
1069
- const raw = typeof manifest?.skills === 'string' && manifest.skills.trim() !== ''
1070
- ? manifest.skills
1071
- : plugin.manifest.skills ?? './codex/skills';
1072
- const normalized = path.posix.normalize(raw.replace(/^\.\//u, ''));
1073
- if (normalized === '..' || normalized.startsWith('../') || path.isAbsolute(normalized)) {
1074
- throw new Error(`Codex plugin skills root 越界:${raw}`);
1075
- }
1076
- return normalized;
1077
- }
1078
1235
  async function buildClaudeWrites(options) {
1079
1236
  const paths = resolveClaudePaths(options.cwd, options.scope, options.env);
1080
1237
  const writes = [];
@@ -1083,8 +1240,9 @@ async function buildClaudeWrites(options) {
1083
1240
  platform: 'claude',
1084
1241
  previousArtifacts: options.historicalArtifacts,
1085
1242
  });
1243
+ const commandCandidates = [...(options.commandCandidates ?? [])];
1244
+ const preparedSkills = [];
1086
1245
  for (const artifact of dynamic.artifacts.filter((entry) => entry.target === 'claude.skill')) {
1087
- const skillKey = artifact.skillKey;
1088
1246
  const tree = await createDynamicSkillExpectedTree({
1089
1247
  artifact,
1090
1248
  sourceDirs: dynamic.sourceDirs(artifact),
@@ -1093,6 +1251,18 @@ async function buildClaudeWrites(options) {
1093
1251
  sourceRequirementsReceiptPolicy: 'allow',
1094
1252
  });
1095
1253
  await injectInstalledRuntimeConfig(tree, dynamic.runtimeConfigSnapshots?.[artifact.id]);
1254
+ const candidate = await prepareSkillCommandCandidate({ ...options, producer: { kind: 'artifact', artifactId: artifact.id }, platform: 'claude', tree, artifacts: dynamic.artifacts });
1255
+ if (candidate)
1256
+ commandCandidates.push(candidate);
1257
+ preparedSkills.push({ artifact, tree });
1258
+ }
1259
+ const staticSkills = await prepareStaticNativeSkillActions({ ...options, platform: 'claude', artifacts: dynamic.artifacts, commandCandidates });
1260
+ commandCandidates.push(...staticSkills.candidates);
1261
+ options.nativeSkillActions?.push(...staticSkills.actions);
1262
+ const commandResolver = await createManagedCommandReferenceResolver({ ...options, platform: 'claude', candidates: commandCandidates });
1263
+ for (const { artifact, tree } of preparedSkills) {
1264
+ const skillKey = artifact.skillKey;
1265
+ resolveSkillManagedCommandReferences(tree, commandResolver);
1096
1266
  assertNoSensitiveConfigInFiles(withoutVerifiedRequirementsReceipt(options.plugin, tree.files), dynamic.sensitiveValues, 'artifactRendererSensitiveConfigExposed: Final dynamic Skill contains sensitive config.');
1097
1267
  writes.push(dynamicSkillWrite({
1098
1268
  ...options,
@@ -1121,7 +1291,7 @@ async function buildClaudeWrites(options) {
1121
1291
  scope: options.scope,
1122
1292
  settingsPath: paths.settingsPath,
1123
1293
  });
1124
- const desiredPermissions = mergeClaudePermissions(permissions);
1294
+ const desiredPermissions = mergeClaudePermissions(permissions, commandResolver);
1125
1295
  const mutation = { kind: 'claude-permissions', permissions: desiredPermissions };
1126
1296
  writes.push({
1127
1297
  artifact: { path: paths.settingsPath, kind: 'permission', platform: 'claude' },
@@ -1151,6 +1321,8 @@ async function buildCodexWrites(options) {
1151
1321
  const dynamicInstructions = dynamic.artifacts.filter((entry) => entry.target === 'codex.developerInstructionsBlock');
1152
1322
  const dynamicRoles = dynamic.artifacts.filter((entry) => entry.target === 'codex.agentRole');
1153
1323
  const dynamicSkills = dynamic.artifacts.filter((entry) => entry.target === 'codex.skill');
1324
+ const commandCandidates = [...(options.commandCandidates ?? [])];
1325
+ const preparedSkills = [];
1154
1326
  const repoRoot = await resolveRepoRoot(options.cwd);
1155
1327
  const skillRoot = options.scope === 'user'
1156
1328
  ? path.join(getCodexHome(options.env), 'skills')
@@ -1164,6 +1336,17 @@ async function buildCodexWrites(options) {
1164
1336
  sourceRequirementsReceiptPolicy: 'allow',
1165
1337
  });
1166
1338
  await injectInstalledRuntimeConfig(tree, dynamic.runtimeConfigSnapshots?.[artifact.id]);
1339
+ const candidate = await prepareSkillCommandCandidate({ ...options, producer: { kind: 'artifact', artifactId: artifact.id }, platform: 'codex', tree, artifacts: dynamic.artifacts });
1340
+ if (candidate)
1341
+ commandCandidates.push(candidate);
1342
+ preparedSkills.push({ artifact, tree });
1343
+ }
1344
+ const staticSkills = await prepareStaticNativeSkillActions({ ...options, platform: 'codex', artifacts: dynamic.artifacts, commandCandidates });
1345
+ commandCandidates.push(...staticSkills.candidates);
1346
+ options.nativeSkillActions?.push(...staticSkills.actions);
1347
+ const commandResolver = await createManagedCommandReferenceResolver({ ...options, platform: 'codex', candidates: commandCandidates });
1348
+ for (const { artifact, tree } of preparedSkills) {
1349
+ resolveSkillManagedCommandReferences(tree, commandResolver);
1167
1350
  assertNoSensitiveConfigInFiles(withoutVerifiedRequirementsReceipt(options.plugin, tree.files), dynamic.sensitiveValues, 'artifactRendererSensitiveConfigExposed: Final dynamic Skill contains sensitive config.');
1168
1351
  writes.push(dynamicSkillWrite({
1169
1352
  ...options,
@@ -1320,7 +1503,7 @@ async function buildCodexWrites(options) {
1320
1503
  } : {}),
1321
1504
  });
1322
1505
  }
1323
- for (const artifact of generateCodexPermissionArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'codex'))) {
1506
+ for (const artifact of generateCodexPermissionArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'codex'), commandResolver)) {
1324
1507
  writes.push(generatedTextWrite({
1325
1508
  ...options,
1326
1509
  platform: 'codex',
@@ -1525,15 +1708,16 @@ async function buildInspectionWrites(options) {
1525
1708
  });
1526
1709
  }
1527
1710
  }
1528
- for (const artifact of generateCodexPermissionArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'codex'))) {
1529
- writes.push(generatedTextWrite({
1530
- ...options,
1531
- configDir: paths.configDir,
1532
- relativePath: artifact.relativePath,
1533
- artifactKey: artifact.artifactKey,
1534
- content: artifact.content,
1535
- kind: 'permission',
1536
- }));
1711
+ for (const permission of resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'codex')) {
1712
+ const artifactKey = buildGeneratedName(options.plugin.manifest.name, permission.name);
1713
+ const artifactPath = path.join(paths.configDir, 'rules', `${artifactKey}.rules`);
1714
+ writes.push({
1715
+ artifact: { path: artifactPath, kind: 'permission', platform: 'codex', artifactKey },
1716
+ component: pathComponent({
1717
+ kind: 'owned-text-file', path: artifactPath, allowedRoot: paths.configDir,
1718
+ identity: ownerIdentity(options.plugin.manifest.name, artifactKey, 'codex', options.scope),
1719
+ }),
1720
+ });
1537
1721
  }
1538
1722
  for (const artifact of generateAgentArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.agents, 'codex'), 'codex')) {
1539
1723
  writes.push(generatedTextWrite({
@@ -2713,9 +2897,20 @@ async function prepareCodexNativeRender(options) {
2713
2897
  .filter((target) => !requested || requested.has(target.id));
2714
2898
  if (targets.length === 0)
2715
2899
  return null;
2900
+ let commandCandidates = [...(options.commandCandidates ?? [])];
2901
+ if (!options.commandCandidates) {
2902
+ for (const declaration of options.plugin.manifest.managedCommands?.packages ?? []) {
2903
+ if (declaration.scope !== 'user' || !declaration.platforms.includes('codex'))
2904
+ continue;
2905
+ const prepared = await prepareManagedCommandPackage({ ...options, packageKey: declaration.key, scope: 'user', cwd: options.plugin.rootDir });
2906
+ commandCandidates.push({ producer: prepared.producer, sets: prepared.sets });
2907
+ await prepared.cleanup();
2908
+ }
2909
+ }
2716
2910
  const byScope = new Map();
2717
2911
  for (const target of targets) {
2718
- const requestScope = target.scopes.includes('user') ? 'user' : 'project';
2912
+ const ownsCommandSet = options.plugin.manifest.managedCommands?.sets.some((set) => set.producer.kind === 'artifact' && set.producer.artifactId === target.id);
2913
+ const requestScope = ownsCommandSet || target.scopes.includes('user') ? 'user' : 'project';
2719
2914
  const group = byScope.get(requestScope) ?? [];
2720
2915
  group.push(target);
2721
2916
  byScope.set(requestScope, group);
@@ -2739,7 +2934,10 @@ async function prepareCodexNativeRender(options) {
2739
2934
  cwd: options.plugin.rootDir,
2740
2935
  commandEnv: options.env,
2741
2936
  previousArtifacts: [],
2742
- requestedArtifactIds: group.map((target) => target.id),
2937
+ requestedArtifactIds: [...group.map((target) => target.id), ...getManagedCommandArtifactTargets(options.plugin.manifest).filter((target) => {
2938
+ const producer = target.producer;
2939
+ return producer.kind === 'artifact' && group.some((skill) => skill.id === producer.artifactId);
2940
+ }).map((target) => target.id)],
2743
2941
  scriptTimeoutMs: options.scriptTimeoutMs,
2744
2942
  });
2745
2943
  rendererDigest = rendered.target.rendererDigest;
@@ -2754,6 +2952,8 @@ async function prepareCodexNativeRender(options) {
2754
2952
  manifestDigest: rendered.target.manifestDigest,
2755
2953
  });
2756
2954
  for (const artifact of validated.artifacts) {
2955
+ if (artifact.target === 'aw.command-set')
2956
+ continue;
2757
2957
  if (artifact.target !== 'codex.skill') {
2758
2958
  throw new Error(`Codex native renderer returned non-Skill artifact "${artifact.id}".`);
2759
2959
  }
@@ -2766,6 +2966,9 @@ async function prepareCodexNativeRender(options) {
2766
2966
  sourceRequirementsReceiptPolicy: 'allow',
2767
2967
  });
2768
2968
  await injectInstalledRuntimeConfig(tree, rendered.runtimeConfigSnapshots[artifact.id], { nativeReceiptPath });
2969
+ const candidate = await prepareSkillCommandCandidate({ plugin: options.plugin, producer: { kind: 'artifact', artifactId: artifact.id }, platform: 'codex', scope: 'user', cwd: options.plugin.rootDir, tree, artifacts: validated.artifacts });
2970
+ if (candidate)
2971
+ commandCandidates.push(candidate);
2769
2972
  assertNoSensitiveConfigInFiles(withoutVerifiedRequirementsReceipt(options.plugin, tree.files), collectSensitivePluginConfigValues(options.plugin.manifest, rendered.context.config.values), 'artifactRendererSensitiveConfigExposed: Final dynamic Skill contains sensitive config.');
2770
2973
  const skillKey = artifact.skillKey;
2771
2974
  renderedArtifacts.push({
@@ -2786,6 +2989,18 @@ async function prepareCodexNativeRender(options) {
2786
2989
  if (!rendererDigest || renderedArtifacts.length !== targets.length) {
2787
2990
  throw new Error('Codex native renderer did not produce the complete declared target set.');
2788
2991
  }
2992
+ if (options.prepareCommandReferences)
2993
+ commandCandidates = await options.prepareCommandReferences(renderedArtifacts.map((entry) => entry.tree), commandCandidates);
2994
+ else {
2995
+ const staticSkills = await prepareStaticNativeSkillActions({ plugin: options.plugin, platform: 'codex', scope: 'user', cwd: options.plugin.rootDir, env: options.env, artifacts: [], commandCandidates });
2996
+ commandCandidates.push(...staticSkills.candidates);
2997
+ }
2998
+ const commandResolver = await createManagedCommandReferenceResolver({ plugin: options.plugin, platform: 'codex', scope: 'user', cwd: options.plugin.rootDir, env: options.env, candidates: commandCandidates });
2999
+ for (const rendered of renderedArtifacts) {
3000
+ resolveSkillManagedCommandReferences(rendered.tree, commandResolver);
3001
+ const receiptTarget = receiptTargets.find((target) => target.id === rendered.artifact.id);
3002
+ receiptTarget.artifactDigest = createCodexSkillStagingTreeContentDigest(rendered.tree);
3003
+ }
2789
3004
  const logicalPluginId = options.plugin.pluginId
2790
3005
  ?? (options.plugin.sourceId ? `${options.plugin.sourceId}/${options.plugin.manifest.name}` : undefined);
2791
3006
  if (!logicalPluginId || !options.plugin.sourceIdentity) {
@@ -2799,6 +3014,7 @@ async function prepareCodexNativeRender(options) {
2799
3014
  targets: receiptTargets,
2800
3015
  });
2801
3016
  return {
3017
+ commandCandidates,
2802
3018
  logicalPluginId,
2803
3019
  sourceIdentity: options.plugin.sourceIdentity,
2804
3020
  rendererDigest,
@@ -2818,6 +3034,7 @@ function prepareCodexNativeRenderCached(options) {
2818
3034
  plugin: options.plugin,
2819
3035
  env: options.context.env,
2820
3036
  scriptTimeoutMs: options.scriptTimeoutMs,
3037
+ commandCandidates: options.context.managedCommandCandidates?.get(pluginActionId(options.plugin)),
2821
3038
  });
2822
3039
  cache.set(options.plugin, preparation);
2823
3040
  }
@@ -2875,8 +3092,12 @@ export async function materializeReviewNativeSkills(options) {
2875
3092
  if (options.skills.length === 0) {
2876
3093
  throw new Error('Review materialization requires at least one selected Skill.');
2877
3094
  }
3095
+ const { prepareIsolatedSkillCommandReferences } = await import('../review/skill-materialization.js');
2878
3096
  const skillsRoot = path.join(options.outputRoot, 'skills');
2879
3097
  await fs.ensureDir(skillsRoot);
3098
+ let commandCandidates = [];
3099
+ const staticTrees = [];
3100
+ const directTrees = [];
2880
3101
  const declarations = options.plugin.manifest.artifacts?.targets ?? [];
2881
3102
  const dynamicSelections = options.skills.filter((skill) => skill.targetId !== null);
2882
3103
  const selectedDeclarations = dynamicSelections.map((selection) => {
@@ -2904,6 +3125,9 @@ export async function materializeReviewNativeSkills(options) {
2904
3125
  errorOnExist: true,
2905
3126
  dereference: false,
2906
3127
  });
3128
+ staticTrees.push(await validateSkillDirectory(destination, `${options.platform}.skill`, {
3129
+ requirementsReceiptPolicy: 'allow',
3130
+ }));
2907
3131
  }
2908
3132
  const directTargetIds = requiredDeclarations
2909
3133
  .filter((target) => target.delivery?.mode !== 'codex-native')
@@ -2917,10 +3141,18 @@ export async function materializeReviewNativeSkills(options) {
2917
3141
  env: options.env,
2918
3142
  event: 'install',
2919
3143
  previousArtifacts: [],
2920
- requestedArtifactIds: directTargetIds,
3144
+ requestedArtifactIds: [
3145
+ ...directTargetIds,
3146
+ ...getManagedCommandArtifactTargets(options.plugin.manifest)
3147
+ .filter((target) => target.producer.kind === 'artifact'
3148
+ && directTargetIds.includes(target.producer.artifactId))
3149
+ .map((target) => target.id),
3150
+ ],
2921
3151
  scriptTimeoutMs: options.scriptTimeoutMs,
2922
3152
  });
2923
3153
  for (const artifact of dynamic.artifacts) {
3154
+ if (artifact.target === 'aw.command-set')
3155
+ continue;
2924
3156
  if (artifact.state !== 'present') {
2925
3157
  throw new Error(`Selected Skill target "${artifact.id}" did not produce a present Skill.`);
2926
3158
  }
@@ -2951,9 +3183,22 @@ export async function materializeReviewNativeSkills(options) {
2951
3183
  }
2952
3184
  await injectInstalledRuntimeConfig(tree, dynamic.runtimeConfigSnapshots?.[artifact.id]);
2953
3185
  assertNoSensitiveConfigInFiles(withoutVerifiedRequirementsReceipt(options.plugin, tree.files), dynamic.sensitiveValues, 'artifactRendererSensitiveConfigExposed: Final dynamic Skill contains sensitive config.');
2954
- await writeDynamicSkillTree(path.join(skillsRoot, tree.skillKey), tree);
3186
+ const candidate = await prepareSkillCommandCandidate({
3187
+ ...options, producer: { kind: 'artifact', artifactId: artifact.id },
3188
+ tree, artifacts: dynamic.artifacts,
3189
+ });
3190
+ if (candidate)
3191
+ commandCandidates.push(candidate);
3192
+ directTrees.push(tree);
2955
3193
  }
2956
3194
  }
3195
+ const prepareReferences = async (trees, scope, copy, candidates = commandCandidates) => prepareIsolatedSkillCommandReferences({
3196
+ ...options, scope, candidates,
3197
+ trees: copy ? trees.map((tree) => ({ ...tree, files: { ...tree.files } })) : trees,
3198
+ });
3199
+ const staticScope = 'user';
3200
+ commandCandidates = await prepareReferences(staticTrees, staticScope, true);
3201
+ commandCandidates = await prepareReferences(directTrees, options.scope, true);
2957
3202
  const nativeTargetIds = requiredDeclarations
2958
3203
  .filter((target) => target.delivery?.mode === 'codex-native')
2959
3204
  .map((target) => target.id);
@@ -2973,10 +3218,13 @@ export async function materializeReviewNativeSkills(options) {
2973
3218
  env: options.env,
2974
3219
  requestedArtifactIds: nativeTargetIds,
2975
3220
  scriptTimeoutMs: options.scriptTimeoutMs,
3221
+ commandCandidates,
3222
+ prepareCommandReferences: (trees, candidates) => prepareReferences(trees, 'user', false, candidates),
2976
3223
  });
2977
3224
  if (!prepared) {
2978
3225
  throw new Error('Selected Codex native Skill targets did not produce a projection.');
2979
3226
  }
3227
+ commandCandidates = prepared.commandCandidates;
2980
3228
  const staged = await materializeCodexNativeRender({
2981
3229
  plugin: options.plugin,
2982
3230
  stagingRoot: marketplaceRoot,
@@ -3007,6 +3255,18 @@ export async function materializeReviewNativeSkills(options) {
3007
3255
  }
3008
3256
  await fs.remove(marketplaceRoot);
3009
3257
  }
3258
+ commandCandidates = await prepareReferences(staticTrees, staticScope, false);
3259
+ commandCandidates = await prepareReferences(directTrees, options.scope, false);
3260
+ for (const tree of staticTrees) {
3261
+ for (const [relativePath, content] of Object.entries(tree.files)) {
3262
+ if (relativePath.endsWith('.md')) {
3263
+ await fs.writeFile(path.join(skillsRoot, tree.skillKey, ...relativePath.split('/')), content);
3264
+ }
3265
+ }
3266
+ }
3267
+ for (const tree of directTrees) {
3268
+ await writeDynamicSkillTree(path.join(skillsRoot, tree.skillKey), tree);
3269
+ }
3010
3270
  const skills = [];
3011
3271
  for (const skill of selectedNames.sort((left, right) => left.localeCompare(right))) {
3012
3272
  const skillRoot = path.join(skillsRoot, skill);
@@ -3020,7 +3280,7 @@ export async function materializeReviewNativeSkills(options) {
3020
3280
  }
3021
3281
  return { rootDir: options.outputRoot, skills };
3022
3282
  }
3023
- async function mirrorCatalog(catalog, env, target, logicalIdentity, prepareStaging, rematerializedPluginIds = new Set()) {
3283
+ async function mirrorCatalog(catalog, env, target, logicalIdentity, prepareStaging, rematerializedPluginIds = new Set(), rematerializedPluginPlatforms) {
3024
3284
  if (!target?.sourceId || !target.marketplaceRoot) {
3025
3285
  const rootDir = resolveAwHome(env);
3026
3286
  await fs.ensureDir(rootDir);
@@ -3076,6 +3336,7 @@ async function mirrorCatalog(catalog, env, target, logicalIdentity, prepareStagi
3076
3336
  return false;
3077
3337
  throw error;
3078
3338
  });
3339
+ const retainedReceipts = [];
3079
3340
  if (destinationExists) {
3080
3341
  await replayNamespacedCodexSkillOverlays({
3081
3342
  sourceRoot: destination,
@@ -3091,9 +3352,13 @@ async function mirrorCatalog(catalog, env, target, logicalIdentity, prepareStagi
3091
3352
  sourceIdentity: target.sourceIdentity,
3092
3353
  marketplaceName,
3093
3354
  rematerializedPluginIds,
3355
+ rematerializedPluginPlatforms,
3356
+ onPreservedPlatformReceipt: (receipt) => retainedReceipts.push(receipt),
3094
3357
  });
3095
3358
  }
3096
3359
  await prepareStaging?.(staging);
3360
+ for (const receipt of retainedReceipts)
3361
+ await refreshPreservedPlatformReceipt(staging, receipt);
3097
3362
  const backup = `${destination}.backup-${crypto.randomUUID()}`;
3098
3363
  if (destinationExists)
3099
3364
  await fs.move(destination, backup);
@@ -3187,6 +3452,7 @@ async function applyPlannedOperationAction(action) {
3187
3452
  if (digest !== action.contentDigest)
3188
3453
  throw new Error('Skill action content digest mismatch.');
3189
3454
  const treeFiles = {};
3455
+ const fileModes = {};
3190
3456
  for (const file of files) {
3191
3457
  const content = Buffer.from(file.contentBase64, 'base64');
3192
3458
  if (`sha256:${crypto.createHash('sha256').update(content).digest('hex')}` !== file.contentDigest) {
@@ -3194,10 +3460,15 @@ async function applyPlannedOperationAction(action) {
3194
3460
  }
3195
3461
  if (file.relativePath !== '.agent-workflows-owner.json')
3196
3462
  treeFiles[file.relativePath] = content;
3463
+ if (file.mode !== undefined && file.mode !== 0o644 && file.mode !== 0o755)
3464
+ throw new Error('Skill action file mode is invalid.');
3465
+ if (file.mode === 0o755 && file.relativePath !== '.agent-workflows-owner.json')
3466
+ fileModes[file.relativePath] = 0o755;
3197
3467
  }
3198
3468
  await writeDynamicSkillTree(action.path, {
3199
3469
  skillKey: action.owner.artifactKey,
3200
3470
  files: treeFiles,
3471
+ ...(Object.keys(fileModes).length > 0 ? { fileModes } : {}),
3201
3472
  }, {
3202
3473
  allowedRoot: action.allowedRoot,
3203
3474
  identity: action.owner,
@@ -3265,6 +3536,88 @@ async function applyPlannedOperationAction(action) {
3265
3536
  }
3266
3537
  function createNativeDriver(options) {
3267
3538
  const runner = options.context.nativeCliRunner;
3539
+ const preflightCommandBindings = async (targets) => {
3540
+ const producers = [];
3541
+ for (const target of targets)
3542
+ for (const action of target.operationActions ?? []) {
3543
+ if (action.kind === 'syncManagedCommandPackage') {
3544
+ producers.push({ producer: action.producer, sets: action.sets });
3545
+ }
3546
+ else if (action.kind === 'materializeNativeSkill'
3547
+ || (action.kind === 'writeOwnedArtifact' && action.artifactKind === 'skill-directory')) {
3548
+ const file = action.files?.find((entry) => entry.relativePath === COMMAND_PRODUCER_RECORD_FILE);
3549
+ if (file)
3550
+ producers.push(normalizeInstalledCommandProducer(JSON.parse(Buffer.from(file.contentBase64, 'base64').toString('utf8'))));
3551
+ }
3552
+ }
3553
+ await assertManagedCommandBindingIdentities({ producers, env: options.context.env });
3554
+ };
3555
+ const revokeAtRoot = async (root, recursive = false, platform) => {
3556
+ const listed = await listManagedCommands(options.context.env);
3557
+ const producers = new Map();
3558
+ for (const command of listed.commands) {
3559
+ if (command.owner.kind !== 'plugin' || !command.resourceRoot)
3560
+ continue;
3561
+ if (platform && command.owner.producer.platform !== platform)
3562
+ continue;
3563
+ const relative = path.relative(path.resolve(root), path.resolve(command.resourceRoot));
3564
+ if (relative !== '' && (!recursive || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)))
3565
+ continue;
3566
+ producers.set(getCommandPackageId(command.owner.producer), command.owner.producer);
3567
+ }
3568
+ for (const producer of producers.values()) {
3569
+ const removedCommandIds = await revokeManagedCommandBindings(producer, options.context.env);
3570
+ options.context.commandResults ??= [];
3571
+ options.context.commandResults.push({ producer, state: 'completed', commandIds: [], removedCommandIds, errors: [], recoveryActions: [] });
3572
+ }
3573
+ };
3574
+ const publishPackages = async (target) => {
3575
+ const cache = options.context.managedCommandProducerCache ??= new Map();
3576
+ for (const action of target.operationActions ?? []) {
3577
+ if (action.kind !== 'syncManagedCommandPackage')
3578
+ continue;
3579
+ const key = getCommandPackageId(action.producer);
3580
+ let publication = cache.get(key);
3581
+ if (!publication) {
3582
+ publication = (async () => {
3583
+ await options.context.reporter.event({ type: 'managed-command:producer', source: { plugin: action.producer.pluginName, scope: action.producer.scope }, data: { packageKey: action.producer.key, phase: 'start' } });
3584
+ const temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'aw-command-publish-'));
3585
+ try {
3586
+ const digest = `sha256:${crypto.createHash('sha256').update(JSON.stringify(action.files)).digest('hex')}`;
3587
+ if (digest !== action.contentDigest)
3588
+ throw new ManagedCommandError('commandResourceInvalid', 'The prepared command package digest changed.');
3589
+ const resourceRoot = path.join(temporary, 'resource');
3590
+ await fs.ensureDir(resourceRoot);
3591
+ for (const file of action.files) {
3592
+ const content = Buffer.from(file.contentBase64, 'base64');
3593
+ if (`sha256:${crypto.createHash('sha256').update(content).digest('hex')}` !== file.contentDigest)
3594
+ throw new ManagedCommandError('commandResourceInvalid', 'The prepared command package file digest changed.');
3595
+ const destination = path.join(resourceRoot, ...file.relativePath.split('/'));
3596
+ await fs.outputFile(destination, content);
3597
+ await fs.chmod(destination, file.mode);
3598
+ }
3599
+ const result = await syncManagedCommandPackage({ producer: action.producer, sourceDirectory: resourceRoot, sets: action.sets, env: options.context.env });
3600
+ await options.context.reporter.event({ type: 'managed-command:producer', source: { plugin: action.producer.pluginName, scope: action.producer.scope }, data: { packageKey: action.producer.key, phase: 'success' } });
3601
+ return { producer: action.producer, state: 'completed', commandIds: result.commandIds, removedCommandIds: result.removedCommandIds, errors: [], recoveryActions: [] };
3602
+ }
3603
+ catch (error) {
3604
+ await options.context.reporter.event({ type: 'managed-command:producer', source: { plugin: action.producer.pluginName, scope: action.producer.scope }, data: { packageKey: action.producer.key, phase: 'failure' } });
3605
+ return { producer: action.producer, state: 'failed', commandIds: [], removedCommandIds: [], errors: [managedCommandDiagnostic(error)], recoveryActions: [`aw command package sync ${action.producer.sourceId}/${action.producer.pluginName} ${action.producer.key} --scope ${action.producer.scope} --yes`] };
3606
+ }
3607
+ finally {
3608
+ await fs.remove(temporary);
3609
+ }
3610
+ })();
3611
+ cache.set(key, publication);
3612
+ }
3613
+ const result = await publication;
3614
+ options.context.commandResults ??= [];
3615
+ if (!options.context.commandResults.some((entry) => getCommandPackageId(entry.producer) === key))
3616
+ options.context.commandResults.push(result);
3617
+ if (result.state !== 'completed')
3618
+ throw new Error('commandPackagePublishFailed: sync the command package and retry the plugin operation.');
3619
+ }
3620
+ };
3268
3621
  const currentCodexManager = () => options.context.codexManager;
3269
3622
  const nativeGroupPreparations = new Map();
3270
3623
  const nativeTargetPreparations = new WeakMap();
@@ -3279,7 +3632,6 @@ function createNativeDriver(options) {
3279
3632
  target.identity.sourceIdentity ?? '',
3280
3633
  target.identity.marketplaceName ?? MARKETPLACE_NAME,
3281
3634
  path.normalize(path.resolve(target.identity.marketplaceRoot ?? resolveAwHome(options.context.env))),
3282
- target.identity.platform,
3283
3635
  ].join('\0');
3284
3636
  const observationScopeFor = (target) => codexObservationScope(options.context, target.identity.marketplaceName ?? MARKETPLACE_NAME, target.identity.scope);
3285
3637
  const groupTargets = (target) => {
@@ -3318,11 +3670,12 @@ function createNativeDriver(options) {
3318
3670
  preparation = (async () => {
3319
3671
  const members = groupTargets(target);
3320
3672
  const materializing = members.filter((candidate) => candidate.writeNativeRequired ?? candidate.requiresNative);
3321
- if (target.identity.platform === 'codex') {
3322
- const observationScope = codexObservationScope(options.context, target.identity.marketplaceName ?? MARKETPLACE_NAME, members.some((candidate) => candidate.identity.scope === 'project') ? 'project' : 'user');
3323
- const nativePluginIds = [...new Set(members.flatMap((candidate) => candidate.ownedComponents.flatMap((component) => component.kind === 'native-plugin' ? [component.pluginId] : [])))].sort();
3673
+ const codexMembers = members.filter((candidate) => candidate.identity.platform === 'codex');
3674
+ if (codexMembers.length) {
3675
+ const observationScope = codexObservationScope(options.context, target.identity.marketplaceName ?? MARKETPLACE_NAME, codexMembers.some((candidate) => candidate.identity.scope === 'project') ? 'project' : 'user');
3676
+ const nativePluginIds = [...new Set(codexMembers.flatMap((candidate) => candidate.ownedComponents.flatMap((component) => component.kind === 'native-plugin' ? [component.pluginId] : [])))].sort();
3324
3677
  for (const pluginId of nativePluginIds) {
3325
- const member = members.find((candidate) => candidate.ownedComponents.some((component) => component.kind === 'native-plugin' && component.pluginId === pluginId));
3678
+ const member = codexMembers.find((candidate) => candidate.ownedComponents.some((component) => component.kind === 'native-plugin' && component.pluginId === pluginId));
3326
3679
  const registration = await inspectCodexPluginRegistrationLive({
3327
3680
  pluginId,
3328
3681
  cwd: member.identity.cwd,
@@ -3337,6 +3690,7 @@ function createNativeDriver(options) {
3337
3690
  }
3338
3691
  if (materializing.length === 0)
3339
3692
  return;
3693
+ await preflightCommandBindings(materializing);
3340
3694
  const first = materializing[0];
3341
3695
  if (!first.catalog)
3342
3696
  throw new Error('Missing rebuild target catalog data.');
@@ -3353,8 +3707,35 @@ function createNativeDriver(options) {
3353
3707
  right.identity.scope,
3354
3708
  right.identity.cwd,
3355
3709
  ].join('\0'), 'en'));
3356
- const transaction = await mirrorCatalog(first.catalog, options.context.env, first.identity, first.plugin.catalogLogicalIdentity, first.identity.platform === 'codex' ? async (stagingRoot) => {
3710
+ const requestedPlatforms = new Map();
3711
+ for (const candidate of materializationTargets) {
3712
+ const pluginId = candidate.identity.pluginId ?? `${candidate.identity.sourceId}/${candidate.identity.pluginName}`;
3713
+ const platforms = requestedPlatforms.get(pluginId) ?? new Set();
3714
+ platforms.add(candidate.identity.platform);
3715
+ requestedPlatforms.set(pluginId, platforms);
3716
+ }
3717
+ const transaction = await mirrorCatalog(first.catalog, options.context.env, first.identity, first.plugin.catalogLogicalIdentity, async (stagingRoot) => {
3718
+ const renderedReceipts = [];
3357
3719
  for (const candidate of materializationTargets) {
3720
+ for (const action of candidate.operationActions ?? []) {
3721
+ if (action.kind !== 'materializeNativeSkill' || action.artifactId !== undefined)
3722
+ continue;
3723
+ const skillsRoot = path.join(stagingRoot, 'plugins', candidate.identity.pluginName, action.platform, candidate.plugin.manifest.skills.replace(/^\.\//u, ''), action.skillKey);
3724
+ if (`sha256:${crypto.createHash('sha256').update(JSON.stringify(action.files)).digest('hex')}` !== action.contentDigest)
3725
+ throw new Error('commandResourceInvalid: prepared static Skill snapshot changed.');
3726
+ for (const file of action.files) {
3727
+ const content = Buffer.from(file.contentBase64, 'base64');
3728
+ if (`sha256:${crypto.createHash('sha256').update(content).digest('hex')}` !== file.contentDigest)
3729
+ throw new Error('commandResourceInvalid: prepared static Skill file digest changed.');
3730
+ if (file.mode !== undefined && file.mode !== 0o644 && file.mode !== 0o755)
3731
+ throw new Error('Skill action file mode is invalid.');
3732
+ const destination = path.join(skillsRoot, ...file.relativePath.split('/'));
3733
+ await fs.outputFile(destination, content);
3734
+ await fs.chmod(destination, file.mode ?? 0o644);
3735
+ }
3736
+ }
3737
+ if (candidate.identity.platform !== 'codex')
3738
+ continue;
3358
3739
  const hasStaticSkills = candidate.plugin.manifest.skills !== undefined;
3359
3740
  const preparedNativeRender = await requirePreparedCodexNativeRender({
3360
3741
  context: options.context,
@@ -3368,6 +3749,8 @@ function createNativeDriver(options) {
3368
3749
  hasStaticSkills,
3369
3750
  })
3370
3751
  : null;
3752
+ if (nativeRender)
3753
+ renderedReceipts.push({ pluginName: candidate.identity.pluginName, receipt: nativeRender.receipt });
3371
3754
  if (!nativeRender) {
3372
3755
  await ensureCodexActiveSkillsManifest({
3373
3756
  plugin: candidate.plugin,
@@ -3385,7 +3768,9 @@ function createNativeDriver(options) {
3385
3768
  });
3386
3769
  }
3387
3770
  }
3388
- } : undefined, new Set(materializationTargets.map((candidate) => candidate.identity.pluginId ?? `${candidate.identity.sourceId}/${candidate.identity.pluginName}`)));
3771
+ for (const receipt of renderedReceipts)
3772
+ await refreshPreservedPlatformReceipt(stagingRoot, receipt);
3773
+ }, new Set(materializationTargets.map((candidate) => candidate.identity.pluginId ?? `${candidate.identity.sourceId}/${candidate.identity.pluginName}`)), requestedPlatforms);
3389
3774
  try {
3390
3775
  await transaction.commit();
3391
3776
  }
@@ -3401,6 +3786,8 @@ function createNativeDriver(options) {
3401
3786
  throw error;
3402
3787
  }
3403
3788
  committedNativeGroups.add(key);
3789
+ for (const candidate of materializationTargets)
3790
+ await revokeAtRoot(path.join(candidate.identity.marketplaceRoot ?? resolveAwHome(options.context.env), 'plugins', candidate.identity.pluginName), true, candidate.identity.platform);
3404
3791
  })().catch((error) => {
3405
3792
  throw asNativeGroupFailure(error);
3406
3793
  });
@@ -3631,7 +4018,7 @@ function createNativeDriver(options) {
3631
4018
  await validateSkillDirectory(component.path, component.identity.platform === 'codex' ? 'codex.skill' : 'claude.skill', { ownerMarkerPolicy: 'ignore', requirementsReceiptPolicy: 'allow' });
3632
4019
  },
3633
4020
  skillDirectoryMatchesPristineTree: async ({ actualPath, files }) => {
3634
- const expectedFiles = new Map(files.map((entry) => [entry.relativePath, entry.contentDigest]));
4021
+ const expectedFiles = new Map(files.map((entry) => [entry.relativePath, entry]));
3635
4022
  const expectedDirectories = new Set(['']);
3636
4023
  for (const relativePath of expectedFiles.keys()) {
3637
4024
  const segments = relativePath.split('/');
@@ -3657,7 +4044,11 @@ function createNativeDriver(options) {
3657
4044
  if (!entry.isFile())
3658
4045
  return false;
3659
4046
  const digest = `sha256:${crypto.createHash('sha256').update(await fs.readFile(absolutePath)).digest('hex')}`;
3660
- if (expectedFiles.get(relativePath) !== digest)
4047
+ const expected = expectedFiles.get(relativePath);
4048
+ if (expected?.contentDigest !== digest)
4049
+ return false;
4050
+ if (catalogFileMode(relativePath, (await fs.stat(absolutePath)).mode)
4051
+ !== catalogFileMode(relativePath, expected.mode ?? 0o644))
3661
4052
  return false;
3662
4053
  seen.add(relativePath);
3663
4054
  }
@@ -3667,6 +4058,10 @@ function createNativeDriver(options) {
3667
4058
  };
3668
4059
  return {
3669
4060
  ownership,
4061
+ beforeCleanupMutation: async (component) => {
4062
+ if (component.kind === 'skill-directory')
4063
+ await revokeAtRoot(component.path);
4064
+ },
3670
4065
  setNativeMarketplaceEligibleTargets: (eligibleTargets) => {
3671
4066
  nativeMarketplaceEligibleTargets = new Set(eligibleTargets);
3672
4067
  },
@@ -3760,6 +4155,7 @@ function createNativeDriver(options) {
3760
4155
  : 'user'))));
3761
4156
  },
3762
4157
  preflightMutation: async (target) => {
4158
+ await preflightCommandBindings([target]);
3763
4159
  if (!target.codexNativeObservationExpectation
3764
4160
  && !target.codexNativeSensitivePreflightRequired)
3765
4161
  return;
@@ -3827,7 +4223,15 @@ function createNativeDriver(options) {
3827
4223
  });
3828
4224
  }
3829
4225
  },
3830
- finalizeCleanup: async () => undefined,
4226
+ finalizeCleanup: async (target) => {
4227
+ for (const action of target.operationActions ?? []) {
4228
+ if (action.kind !== 'retainManagedCommandPackage')
4229
+ continue;
4230
+ options.context.commandResults ??= [];
4231
+ if (!options.context.commandResults.some((result) => getCommandPackageId(result.producer) === action.packageId))
4232
+ options.context.commandResults.push(retainedManagedCommandPackageResult(action));
4233
+ }
4234
+ },
3831
4235
  probeNative: async (component) => {
3832
4236
  const target = findTarget(component);
3833
4237
  if (!target)
@@ -3915,8 +4319,10 @@ function createNativeDriver(options) {
3915
4319
  throw new Error('codexNativeObservationUnavailable: native group precheck is missing.');
3916
4320
  }
3917
4321
  const removalRequired = before === 'present';
3918
- if (!removalRequired)
4322
+ if (!removalRequired) {
4323
+ await revokeAtRoot(path.join(target.identity.marketplaceRoot ?? resolveAwHome(options.context.env), 'plugins', target.identity.pluginName), true, 'codex');
3919
4324
  return;
4325
+ }
3920
4326
  try {
3921
4327
  codexMutationMarketplaces.add(marketplaceName);
3922
4328
  await runObservedCodexMutation({
@@ -3945,6 +4351,8 @@ function createNativeDriver(options) {
3945
4351
  ? `codexNativeRemoveIncomplete: ${CODEX_REMOVE_RECOVERY}`
3946
4352
  : `codexNativeInstallIncomplete: ${component.pluginId} is absent from Codex plugin list.`);
3947
4353
  }
4354
+ if (expected === 'absent')
4355
+ await revokeAtRoot(path.join(target.identity.marketplaceRoot ?? resolveAwHome(options.context.env), 'plugins', target.identity.pluginName), true, 'codex');
3948
4356
  };
3949
4357
  if (options.operationScopedNativePhases) {
3950
4358
  deferredPostchecks.push({ target, postcheck });
@@ -3980,6 +4388,7 @@ function createNativeDriver(options) {
3980
4388
  }
3981
4389
  },
3982
4390
  writeNative: async (target) => {
4391
+ await publishPackages(target);
3983
4392
  if (!target.catalog)
3984
4393
  throw new Error('Missing rebuild target catalog data.');
3985
4394
  const marketplaceName = target.identity.marketplaceName ?? MARKETPLACE_NAME;
@@ -4082,13 +4491,48 @@ function createNativeDriver(options) {
4082
4491
  throw new Error(`Claude native install postcheck 失败:declaredMarketplace=${after.marketplaceDeclaredState},materializedMarketplace=${after.marketplaceMaterializedState},plugin=${after.pluginState},enabled=${after.pluginEnabled},versions=${after.pluginVersions.join(',')}。`);
4083
4492
  }
4084
4493
  }
4494
+ const pluginRoot = path.join(expectedMarketplaceRoot, 'plugins', target.identity.pluginName);
4495
+ if (target.identity.platform === 'codex' || target.operationActions?.some((action) => action.kind === 'materializeNativeSkill' && action.platform === 'claude')) {
4496
+ const relativeRoot = target.identity.platform === 'codex'
4497
+ ? await resolveCodexSkillsRelativeRoot(target.plugin, expectedMarketplaceRoot)
4498
+ : await resolveClaudeSkillsRelativeRoot(pluginRoot);
4499
+ const skillsRoot = path.join(pluginRoot, relativeRoot);
4500
+ for (const entry of await fs.readdir(skillsRoot, { withFileTypes: true }).catch(() => [])) {
4501
+ if (!entry.isDirectory())
4502
+ continue;
4503
+ const resourceRoot = path.join(skillsRoot, entry.name);
4504
+ const installed = await readInstalledCommandProducer(resourceRoot).catch(() => null);
4505
+ if (!installed)
4506
+ continue;
4507
+ const commandIds = await publishManagedCommandBindings({ ...installed, resourceRoot, env: options.context.env });
4508
+ options.context.commandResults ??= [];
4509
+ options.context.commandResults.push({ producer: installed.producer, state: 'completed', commandIds, removedCommandIds: [], errors: [], recoveryActions: [] });
4510
+ }
4511
+ }
4085
4512
  },
4086
4513
  writeArtifacts: async (target) => {
4514
+ await preflightCommandBindings([target]);
4515
+ await publishPackages(target);
4087
4516
  for (const operationAction of target.operationActions ?? []) {
4088
4517
  if (operationAction.kind === 'writeOwnedArtifact'
4089
4518
  || operationAction.kind === 'updateSharedConfig'
4090
4519
  || operationAction.kind === 'setActivation') {
4520
+ if (operationAction.kind === 'writeOwnedArtifact' && operationAction.artifactKind === 'skill-directory') {
4521
+ const previous = await readInstalledCommandProducer(operationAction.path).catch(() => null);
4522
+ if (previous && (previous.producer.sourceId !== target.identity.sourceId || previous.producer.sourceIdentity !== target.identity.sourceIdentity || previous.producer.pluginName !== target.identity.pluginName))
4523
+ throw new Error('commandOwnerMismatch: installed command producer belongs to another source identity; clean up the previous owner first.');
4524
+ }
4091
4525
  await applyPlannedOperationAction(operationAction);
4526
+ if (operationAction.kind === 'writeOwnedArtifact' && operationAction.artifactKind === 'skill-directory') {
4527
+ const installed = await readInstalledCommandProducer(operationAction.path).catch(() => null);
4528
+ if (installed) {
4529
+ const commandIds = await publishManagedCommandBindings({ ...installed, resourceRoot: operationAction.path, env: options.context.env });
4530
+ options.context.commandResults ??= [];
4531
+ options.context.commandResults.push({ producer: installed.producer, state: 'completed', commandIds, removedCommandIds: [], errors: [], recoveryActions: [] });
4532
+ }
4533
+ else
4534
+ await revokeAtRoot(operationAction.path);
4535
+ }
4092
4536
  }
4093
4537
  }
4094
4538
  return target.artifacts;
@@ -4172,6 +4616,26 @@ export function createRebuildIntegration(options) {
4172
4616
  });
4173
4617
  }
4174
4618
  const event = 'install';
4619
+ const commands = materializing ? await commandPackageActions({ ...input, env: options.context.env, context: options.context }) : { actions: [], candidates: [] };
4620
+ options.context.managedCommandCandidates ??= new Map();
4621
+ options.context.managedCommandCandidates.set(pluginActionId(input.plugin), commands.candidates);
4622
+ const nativeSkillActions = [];
4623
+ if (materializing && input.platform === 'codex' && input.plugin.manifest.managedCommands && codexNativeRenderTargets(input.plugin).length) {
4624
+ const native = await prepareCodexNativeRenderCached({ context: options.context, plugin: input.plugin, scriptTimeoutMs: options.scriptTimeoutMs });
4625
+ if (native) {
4626
+ commands.candidates.push(...native.commandCandidates);
4627
+ for (const { artifact, skillKey, tree } of native.renderedArtifacts) {
4628
+ const files = Object.entries(tree.files).map(([relativePath, content]) => ({
4629
+ relativePath, contentBase64: content.toString('base64'),
4630
+ contentDigest: `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`,
4631
+ ...(tree.fileModes?.[relativePath] ? { mode: tree.fileModes[relativePath] } : {}),
4632
+ })).sort((left, right) => left.relativePath.localeCompare(right.relativePath));
4633
+ nativeSkillActions.push({ kind: 'materializeNativeSkill', pluginId: pluginActionId(input.plugin),
4634
+ platform: 'codex', scope: 'user', artifactId: artifact.id, skillKey, files,
4635
+ contentDigest: `sha256:${crypto.createHash('sha256').update(JSON.stringify(files)).digest('hex')}` });
4636
+ }
4637
+ }
4638
+ }
4175
4639
  const historicalArtifacts = [];
4176
4640
  let writes = [];
4177
4641
  let currentRemovalComponents = [];
@@ -4197,6 +4661,9 @@ export function createRebuildIntegration(options) {
4197
4661
  scriptTimeoutMs: options.scriptTimeoutMs,
4198
4662
  marketplaceName: input.marketplaceName,
4199
4663
  userActivationDesired: options.context.codexUserActivationDesired,
4664
+ commandCandidates: commands.candidates,
4665
+ nativeSkillActions,
4666
+ context: options.context,
4200
4667
  });
4201
4668
  }
4202
4669
  else {
@@ -4206,6 +4673,8 @@ export function createRebuildIntegration(options) {
4206
4673
  event,
4207
4674
  historicalArtifacts,
4208
4675
  scriptTimeoutMs: options.scriptTimeoutMs,
4676
+ commandCandidates: commands.candidates,
4677
+ nativeSkillActions,
4209
4678
  });
4210
4679
  }
4211
4680
  }
@@ -4367,7 +4836,8 @@ export function createRebuildIntegration(options) {
4367
4836
  || codexRegistration.observation.entry.version === input.plugin.manifest.version);
4368
4837
  const materializeNativeRequired = nativeDesired && (input.platform === 'claude'
4369
4838
  || input.command === 'reinstall'
4370
- || !codexNativeExact);
4839
+ || !codexNativeExact
4840
+ || (materializing && await hasPreparedStaticNativeCommandDrift({ plugin: input.plugin, cwd: input.cwd, env: options.context.env, context: options.context, marketplaceRoot: input.marketplaceRoot ?? resolveAwHome(options.context.env) })));
4371
4841
  const nativeCleanupCandidate = input.platform === 'claude'
4372
4842
  ? input.includeNativeCleanupCandidate !== false
4373
4843
  : input.platform === 'codex'
@@ -4456,7 +4926,11 @@ export function createRebuildIntegration(options) {
4456
4926
  ]);
4457
4927
  bindNamespacedOwnerIdentity(allComponents, input);
4458
4928
  const desiredKeys = new Set(desiredComponents.map(componentIdentityKey));
4459
- const operationActions = [];
4929
+ const retainedPackages = input.command === 'cleanup' || input.command === 'uninstall'
4930
+ ? await discoverRetainedManagedCommandPackages({ selectors: [{ sourceId: input.sourceId, pluginName: input.plugin.manifest.name }], scope: input.scope, cwd: input.cwd, env: options.context.env })
4931
+ : [];
4932
+ const operationActions = [...commands.actions, ...nativeSkillActions,
4933
+ ...retainedPackages.map((entry) => ({ ...entry, kind: 'retainManagedCommandPackage', pluginId: `${entry.producer.sourceId}/${entry.producer.pluginName}`, scope: entry.producer.scope }))];
4460
4934
  const preApply = input.plugin.manifest.scripts?.preApply;
4461
4935
  if (preApply && input.command !== 'cleanup' && input.command !== 'uninstall') {
4462
4936
  const script = path.resolve(input.plugin.rootDir, preApply.path.replace(/^\.\//u, ''));