* fix(#4132): verify durable runtime surface sources * chore(#4132): record PR number in changeset * test(#4132): cover rejected commands source alias * fix(#4132): reject aliased package fallback * test(#4132): cover rejected agents source alias * test(#4132): cover partially aliased marker provider * fix(#4132): reject partially aliased source providers * test(#4132): cover routed source identity probes * fix(#4132): route installed source identity probes * refactor(#4132): tighten installer source metadata * test(#4132): cover corpus trust boundary attacks * fix(#4132): close installed corpus trust gaps * refactor(#4132): keep installer authority private * fix(#4132): preserve private installer fallback * test(#4132): preserve fixture source authority * fix(#4132): reject overlapping source fallback * fix(#4132): avoid redundant installed corpus reads * refactor(#4132): simplify provider resolution * test(#4132): sync install tree fixtures after rebase --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -274,6 +274,8 @@ function setCapabilityState(
|
||||
|
||||
// ── Step 4: APPLY PASS ────────────────────────────────────────────────────
|
||||
|
||||
let pendingSurfaceForCommit: SurfaceState | undefined;
|
||||
|
||||
// ── Surface writes ────────────────────────────────────────────────────────
|
||||
if (needsSurface && (idsToDisable.length > 0 || idsToEnable.length > 0)) {
|
||||
const existing = readSurface(resolvedConfigDir);
|
||||
@@ -339,7 +341,7 @@ function setCapabilityState(
|
||||
}
|
||||
|
||||
if (surfaceChanged) {
|
||||
writeSurface(resolvedConfigDir, pendingSurface);
|
||||
pendingSurfaceForCommit = pendingSurface;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +350,13 @@ function setCapabilityState(
|
||||
setConfigValues(cwd, pendingGateWrites);
|
||||
}
|
||||
|
||||
// State-only callers preserve the existing direct-write behavior. A caller
|
||||
// that requests materialization lets applySurface publish the candidate only
|
||||
// after every artifact kind has staged and synchronized successfully.
|
||||
if (pendingSurfaceForCommit && !opts?.materialize) {
|
||||
writeSurface(resolvedConfigDir, pendingSurfaceForCommit);
|
||||
}
|
||||
|
||||
// ── Materialize (optional) ────────────────────────────────────────────────
|
||||
if (opts?.materialize) {
|
||||
const { runtime, scope } = opts.materialize;
|
||||
@@ -376,9 +385,12 @@ function setCapabilityState(
|
||||
// folded:issue-1575-agent-descriptor-parity describe block in
|
||||
// tests/golden-parity-single-source.test.cjs).
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
applySurface(resolvedConfigDir, layout, manifest, undefined, registry, opts?.materialize?.resolveAttribution
|
||||
? { resolveAttribution: opts.materialize.resolveAttribution }
|
||||
: undefined);
|
||||
applySurface(resolvedConfigDir, layout, manifest, undefined, registry, {
|
||||
...(pendingSurfaceForCommit ? { surfaceState: pendingSurfaceForCommit } : {}),
|
||||
...(opts.materialize.resolveAttribution
|
||||
? { resolveAttribution: opts.materialize.resolveAttribution }
|
||||
: {}),
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// Fix C: materialise was explicitly requested — a failure is an error (non-zero exit),
|
||||
|
||||
@@ -68,6 +68,163 @@ const { getDirName } = runtimeNamePolicy;
|
||||
|
||||
type ResolveAttribution = (runtime: string) => any;
|
||||
|
||||
type RuntimeSurfaceSourceClass = 'commands' | 'agents';
|
||||
|
||||
function withInstallerPackageSource<T>(
|
||||
configDir: string,
|
||||
fn: () => T,
|
||||
): T {
|
||||
// Reuse the existing compatibility-marker contract through the existing fs
|
||||
// seam. The marker exists only in this synchronous call tree: no disk state,
|
||||
// layout export, stage argument, or caller-settable authority flag is added.
|
||||
const markerPath = path.resolve(configDir, '.gsd-source');
|
||||
const packageCommandsRoot = runtimeArtifactLayout.findInstallSourceRoot();
|
||||
const markerBytes = Buffer.from(packageCommandsRoot + '\n');
|
||||
const base = installFs();
|
||||
const overlay = {
|
||||
...base,
|
||||
existsSync: (candidate: string): boolean =>
|
||||
path.resolve(candidate) === markerPath || base.existsSync(candidate),
|
||||
lstatSync: (candidate: string): ReturnType<typeof base.lstatSync> =>
|
||||
path.resolve(candidate) === markerPath
|
||||
? { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
|
||||
: base.lstatSync(candidate),
|
||||
readFileSync: ((candidate: string, encoding?: BufferEncoding): string | Buffer => {
|
||||
if (path.resolve(candidate) !== markerPath) {
|
||||
return encoding ? base.readFileSync(candidate, encoding) : base.readFileSync(candidate);
|
||||
}
|
||||
return encoding ? markerBytes.toString(encoding) : Buffer.from(markerBytes);
|
||||
}) as typeof base.readFileSync,
|
||||
};
|
||||
return withInstallFs(overlay, fn);
|
||||
}
|
||||
|
||||
function isRuntimeSurfaceSourceUnavailable(message: string): boolean {
|
||||
return message.startsWith('Runtime Surface source is unavailable or incomplete for ') &&
|
||||
message.endsWith('install or upgrade gsd-core before materializing this surface.');
|
||||
}
|
||||
|
||||
function assertCorpusTreeHasNoSymlinks(root: string): void {
|
||||
if (!installFs().existsSync(root)) return;
|
||||
const stat = installFs().lstatSync(root);
|
||||
if (stat.isSymbolicLink()) {
|
||||
throw new Error(`Runtime Surface corpus path is a symlink: ${root}`);
|
||||
}
|
||||
if (!stat.isDirectory()) return;
|
||||
for (const name of installFs().readdirSync(root)) {
|
||||
assertCorpusTreeHasNoSymlinks(path.join(root, name));
|
||||
}
|
||||
}
|
||||
|
||||
function previousOwnedCorpusFiles(configDir: string, prefix: string): string[] {
|
||||
try {
|
||||
const files = installerMigrations.readInstallManifest(configDir).files;
|
||||
return Object.keys(files)
|
||||
.filter((entry) => entry.startsWith(prefix))
|
||||
.map((entry) => entry.slice(prefix.length))
|
||||
.filter((entry) => entry !== '' && !path.posix.isAbsolute(entry) && !entry.split('/').some((part) => part === '' || part === '.' || part === '..'));
|
||||
} catch {
|
||||
// An absent or unreadable prior manifest provides no ownership evidence.
|
||||
// Preserve existing entries rather than guessing that they are stale.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function pruneEmptyCorpusParents(start: string, stop: string): void {
|
||||
let current = path.dirname(start);
|
||||
while (current !== stop && current.startsWith(stop + path.sep)) {
|
||||
if (installFs().readdirSync(current).length > 0) return;
|
||||
installFs().rmdirSync(current);
|
||||
current = path.dirname(current);
|
||||
}
|
||||
}
|
||||
|
||||
function syncRuntimeSurfaceCorpus(source: string, destination: string, configDir: string, manifestPrefix: string): void {
|
||||
if (hasExistingSymlinkBetween(path.resolve(configDir), destination, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(
|
||||
`syncRuntimeSurfaceCorpus: destination "${destination}" contains a symlink the install root "${configDir}" does not trust — refusing to write.`,
|
||||
);
|
||||
}
|
||||
assertCorpusTreeHasNoSymlinks(destination);
|
||||
|
||||
// Remove only paths the previous manifest proves GSD owned and which the
|
||||
// executing package no longer ships. Unknown neighbouring files survive.
|
||||
for (const relative of previousOwnedCorpusFiles(configDir, manifestPrefix)) {
|
||||
const sourceEntry = path.join(source, ...relative.split('/'));
|
||||
let sourceIsFile = false;
|
||||
try {
|
||||
sourceIsFile = installFs().lstatSync(sourceEntry).isFile();
|
||||
} catch {
|
||||
sourceIsFile = false;
|
||||
}
|
||||
if (sourceIsFile) continue;
|
||||
|
||||
const target = path.join(destination, ...relative.split('/'));
|
||||
if (!installFs().existsSync(target)) continue;
|
||||
const targetStat = installFs().lstatSync(target);
|
||||
if (!targetStat.isFile()) {
|
||||
throw new Error(`Runtime Surface corpus ownership conflict at ${target}`);
|
||||
}
|
||||
installFs().rmSync(target, { force: true });
|
||||
pruneEmptyCorpusParents(target, destination);
|
||||
}
|
||||
|
||||
installFs().mkdirSync(path.dirname(destination), { recursive: true });
|
||||
installFs().cpSync(source, destination, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision the raw, installation-owned input needed to re-materialize a
|
||||
* global Runtime Surface after the executing package tree disappears.
|
||||
*
|
||||
* The corpus deliberately lives below the already-installed `gsd-core/`
|
||||
* tree and is accepted only after its manifest ownership and hashes verify.
|
||||
* The compatibility marker does not replace that installed-corpus authority.
|
||||
*/
|
||||
function provisionRuntimeSurfaceCorpus(
|
||||
layout: { runtime: string; kinds: Iterable<{ kind?: string }> },
|
||||
configDir: string,
|
||||
scope: string,
|
||||
): void {
|
||||
const required = new Set<RuntimeSurfaceSourceClass>();
|
||||
if (isGlobalScope(scope as InstallScope)) {
|
||||
for (const kind of layout.kinds) {
|
||||
if (kind.kind === 'commands' || kind.kind === 'skills') required.add('commands');
|
||||
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') required.add('agents');
|
||||
}
|
||||
}
|
||||
if (required.size === 0) return;
|
||||
|
||||
const corpusRoot = path.join(configDir, 'gsd-core');
|
||||
if (required.has('commands')) {
|
||||
const source = runtimeArtifactLayout.findInstallSourceRoot();
|
||||
const destination = path.join(corpusRoot, 'commands', 'gsd');
|
||||
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/commands/gsd/');
|
||||
}
|
||||
if (required.has('agents')) {
|
||||
const source = path.join(executingPackageRoot(), 'agents');
|
||||
const destination = path.join(corpusRoot, 'agents');
|
||||
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/agents/');
|
||||
}
|
||||
|
||||
const markerFile = _hostBehaviors(layout.runtime).sourceMarkerFile;
|
||||
if (typeof markerFile === 'string' && markerFile !== '' && required.has('commands')) {
|
||||
try {
|
||||
const markerPath = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, markerFile);
|
||||
if (hasExistingSymlinkBetween(path.resolve(configDir), markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(`compatibility marker "${markerPath}" contains an untrusted symlink`);
|
||||
}
|
||||
installFs().writeFileSync(markerPath, path.join(corpusRoot, 'commands', 'gsd') + '\n', 'utf8');
|
||||
} catch {
|
||||
// The existing installer marker writer owns the user-facing warning and
|
||||
// keeps marker failure non-fatal. The installed corpus remains usable
|
||||
// without the compatibility marker.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executingPackageRoot(): string { return path.dirname(path.dirname(runtimeArtifactLayout.findInstallSourceRoot())); }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// USER_OWNED_ARTIFACTS
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1042,9 +1199,15 @@ function installRuntimeArtifacts(
|
||||
resolvedProfile: any,
|
||||
resolveAttribution: ResolveAttribution = () => undefined,
|
||||
capabilityRegistry?: any,
|
||||
deps: { fs?: any; os?: any; env?: Record<string, string | undefined> } = {},
|
||||
deps: { fs?: any; os?: any; env?: Record<string, string | undefined>; packageRoot?: string } = {},
|
||||
): any {
|
||||
return withInstallFs(deps.fs, (): any => {
|
||||
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(
|
||||
runtime,
|
||||
configDir,
|
||||
scope as 'global' | 'local',
|
||||
capabilityRegistry,
|
||||
);
|
||||
// A removed descriptor kind is no longer visited by the layout loop, so it
|
||||
// cannot prune its own previous output. Clean manifest-proven retired files
|
||||
// before materializing the current layout (#2644).
|
||||
@@ -1064,19 +1227,34 @@ function installRuntimeArtifacts(
|
||||
// #2874 design row 2: this early return must ALSO return an executed
|
||||
// plan — installOpencodeFamilyArtifacts reports what it wrote, so a
|
||||
// whole runtime family returning undefined is no longer a hole.
|
||||
return installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors, capabilityRegistry, projectDir);
|
||||
// An injected filesystem supplies its own hermetic corpus fixture. The
|
||||
// real installer is the authority that refreshes package bytes into the
|
||||
// durable installed corpus; attempting that cross-filesystem copy through
|
||||
// an in-memory destination adapter would read from the wrong filesystem.
|
||||
if (!deps.fs) provisionRuntimeSurfaceCorpus(layout, configDir, scope);
|
||||
return installOpencodeFamilyArtifacts(
|
||||
runtime,
|
||||
configDir,
|
||||
scope,
|
||||
resolvedProfile,
|
||||
resolveAttribution,
|
||||
behaviors,
|
||||
capabilityRegistry,
|
||||
deps.packageRoot,
|
||||
projectDir,
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy cleanup before layout-driven writes
|
||||
_runLegacyInstallMigrations(runtime, configDir, scope);
|
||||
|
||||
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope as 'global' | 'local', capabilityRegistry);
|
||||
if (!deps.fs) provisionRuntimeSurfaceCorpus(layout, configDir, scope);
|
||||
// #3712: a global `home` override escapes the sandboxed configDir. Refuse to
|
||||
// execute when a test run would land that escape in the developer's real home.
|
||||
testHomeGuard.assertTestHomeSandboxed('installRuntimeArtifacts', runtime, layout?.kinds, {
|
||||
os: deps.os, env: deps.env,
|
||||
});
|
||||
const planResult = runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
|
||||
const createPlan = () => runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
|
||||
// `Layout` is structurally identical across the layout/install-plan .cjs
|
||||
// modules but nominally distinct to tsc (untyped .cjs boundary) — bridge it.
|
||||
layout: layout as any,
|
||||
@@ -1086,6 +1264,16 @@ function installRuntimeArtifacts(
|
||||
resolveAttribution,
|
||||
projectDir,
|
||||
});
|
||||
let planResult = createPlan();
|
||||
if (
|
||||
scope === 'global' &&
|
||||
!planResult.ok &&
|
||||
planResult.kind === 'stage_failed' &&
|
||||
planResult.cleanupDirs.length === 0 &&
|
||||
isRuntimeSurfaceSourceUnavailable(planResult.message)
|
||||
) {
|
||||
planResult = withInstallerPackageSource(configDir, createPlan);
|
||||
}
|
||||
|
||||
const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
|
||||
// #2874 row 1/4/5: per-kind executed-plan entries, appended only as the
|
||||
@@ -1252,8 +1440,10 @@ function installRuntimeArtifacts(
|
||||
// is safe even when configDir has no .gsd-source marker (artifactLayout: []).
|
||||
let nativePluginInstalled = false;
|
||||
if (behaviors.nativePlugin) {
|
||||
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
|
||||
const src = path.dirname(path.dirname(commandsGsdDir));
|
||||
// Native plugin sources live only in the executing package, never in the
|
||||
// durable Runtime Surface corpus. Do not derive this package root from a
|
||||
// config-scoped provider that may now correctly resolve installed input.
|
||||
const src = deps.packageRoot ?? executingPackageRoot();
|
||||
_installNativePluginIfDeclared(runtime, configDir, behaviors, src);
|
||||
nativePluginInstalled = true;
|
||||
}
|
||||
@@ -1496,8 +1686,11 @@ function installAgentsKindStandalone(
|
||||
capabilityRegistry?: any,
|
||||
projectDir?: string | null,
|
||||
): { sourceDir: string; destDir: string } | null {
|
||||
const layout: any = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope as 'global' | 'local', capabilityRegistry);
|
||||
const agentsKindEntry = layout.kinds.find((k: any) => k.kind === 'agents');
|
||||
const layout: Pick<
|
||||
ReturnType<typeof runtimeArtifactLayout.resolveRuntimeArtifactLayout>,
|
||||
'runtime' | 'kinds'
|
||||
> = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope as 'global' | 'local', capabilityRegistry);
|
||||
const agentsKindEntry = layout.kinds.find((kind) => kind.kind === 'agents');
|
||||
if (!agentsKindEntry) return null;
|
||||
// #3712: this writer selects `agentsKindEntry.home` over targetDir below and then
|
||||
// prunes that destination via _removeGsdEntries, so it is a fifth route into the
|
||||
@@ -1513,7 +1706,13 @@ function installAgentsKindStandalone(
|
||||
// targetDir IS the install root the inline agent loop called `targetDir`.
|
||||
const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
|
||||
const agentCtx = { runtime, pathPrefix, attribution, targetDir, projectDir: projectDir ?? targetDir };
|
||||
const stagedDir: string = agentsKindEntry.stage(resolvedProfile, agentCtx);
|
||||
let stagedDir: string;
|
||||
try {
|
||||
stagedDir = agentsKindEntry.stage(resolvedProfile, agentCtx);
|
||||
} catch (err) {
|
||||
if (scope !== 'global' || !isRuntimeSurfaceSourceUnavailable((err as Error).message)) throw err;
|
||||
stagedDir = withInstallerPackageSource(targetDir, () => agentsKindEntry.stage(resolvedProfile, agentCtx));
|
||||
}
|
||||
|
||||
const stagedAgentFiles: string[] = installFs().existsSync(stagedDir)
|
||||
? installFs().readdirSync(stagedDir).filter((f: string) => f.endsWith('.md'))
|
||||
@@ -1810,6 +2009,7 @@ function installOpencodeFamilyArtifacts(
|
||||
resolveAttribution: ResolveAttribution = () => undefined,
|
||||
behaviors: any = {},
|
||||
capabilityRegistry?: any,
|
||||
packageRoot?: string,
|
||||
projectDir?: string | null,
|
||||
): any {
|
||||
// #2870: `scope` keeps its exported required `string` signature (no
|
||||
@@ -1824,7 +2024,7 @@ function installOpencodeFamilyArtifacts(
|
||||
// into stageSkillsForProfile/stageSkillsForRuntimeAsSkills. The repo/package
|
||||
// root (needed below for the native plugin source) is two levels up.
|
||||
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
|
||||
const src = path.dirname(path.dirname(commandsGsdDir));
|
||||
const src = packageRoot ?? executingPackageRoot();
|
||||
const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsGsdDir, resolvedProfile);
|
||||
|
||||
const pathPrefix = (runtimeArtifactConversion as any)._computePathPrefix({
|
||||
|
||||
@@ -185,11 +185,10 @@ function createRuntimeArtifactInstallPlan(args: CreateRuntimeArtifactInstallPlan
|
||||
resolveAttribution,
|
||||
};
|
||||
|
||||
// ADR-1235 §1: build agentCtx once per plan so agents kind entries can apply
|
||||
// the CORRECT pre-converter cross-cutting (path rewrites → attribution → converter
|
||||
// → normalize). This mirrors the exact per-file order in the inline agent loop
|
||||
// in bin/install.js (lines 9330-9415). agentCtx is passed as the second arg
|
||||
// to kind.stage() for agents kind entries with a converter (convertedAgentsKind).
|
||||
// ADR-1235 §1: build the staging context once per plan. Agent kinds apply
|
||||
// the CORRECT pre-converter cross-cutting (path rewrites → attribution →
|
||||
// converter → normalize). This
|
||||
// mirrors the exact per-file order in the former inline agent loop.
|
||||
// NO _stampNonClaudeRuntimeDefaults — agents are NOT stamped in the inline loop.
|
||||
const os = _require('node:os') as typeof import('node:os');
|
||||
const { posixNormalize } = _require('./shell-command-projection.cjs') as { posixNormalize: (p: string) => string };
|
||||
@@ -214,18 +213,12 @@ function createRuntimeArtifactInstallPlan(args: CreateRuntimeArtifactInstallPlan
|
||||
targetDir: layout.configDir,
|
||||
projectDir: projectDir ?? layout.configDir,
|
||||
};
|
||||
|
||||
for (const kind of layout.kinds) {
|
||||
let stagedDir: string;
|
||||
try {
|
||||
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') {
|
||||
// ADR-1235 §1: pass agentCtx so stageAgentsForRuntimeWithConverter applies
|
||||
// the full inline-loop order: pathRewrites → attribution → converter → normalize.
|
||||
// The cross-cutting is now PRE-converter (inside staging), not POST.
|
||||
stagedDir = kind.stage(resolvedProfile, agentCtx);
|
||||
} else {
|
||||
stagedDir = kind.stage(resolvedProfile);
|
||||
}
|
||||
// Agent kinds use the context for their pre-converter cross-cutting
|
||||
// sequence; other kinds ignore it.
|
||||
stagedDir = kind.stage(resolvedProfile, agentCtx);
|
||||
} catch (err) {
|
||||
return { ok: false, kind: 'stage_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ import os from 'node:os';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import installFsAdapter = require('./install-fs-adapter.cjs');
|
||||
const { installFs, mkInstallTempDir } = installFsAdapter;
|
||||
// Reuse the install manifest's existing parser and streamed SHA-256
|
||||
// classification instead of deriving a second integrity implementation here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import installerMigrations = require('./installer-migrations.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import installProfiles = require('./install-profiles.cjs');
|
||||
const {
|
||||
@@ -92,9 +96,8 @@ interface CapabilityRegistryForSkills {
|
||||
|
||||
/**
|
||||
* Cross-cutting context for descriptor-driven agent staging (ADR-1235 §1).
|
||||
* Passed as the optional second arg to ArtifactKind.stage() for agents kind
|
||||
* entries so that stageAgentsForRuntimeWithConverter can apply the exact
|
||||
* inline-loop transform order: pathRewrites → attribution → converter → normalize.
|
||||
* Agent kinds use these fields for the exact pathRewrites → attribution →
|
||||
* converter → normalize order.
|
||||
*/
|
||||
interface AgentCtx {
|
||||
runtime: string;
|
||||
@@ -113,8 +116,8 @@ interface ArtifactKind {
|
||||
kind: KimiArtifactKindName;
|
||||
destSubpath: string;
|
||||
prefix: string;
|
||||
/** For agents kind with a converter, accepts an optional AgentCtx as the second
|
||||
* arg so cross-cutting can be applied pre-converter (ADR-1235 §1). */
|
||||
/** For agent kinds, accepts optional pre-converter cross-cutting context
|
||||
* (ADR-1235 §1). */
|
||||
stage: (resolvedProfile: ResolvedProfile, agentCtx?: AgentCtx) => string;
|
||||
/** Resolved absolute alternate install root for this kind, if the descriptor
|
||||
* specifies one (e.g. codex skills → $HOME/.agents). Undefined means the
|
||||
@@ -128,6 +131,31 @@ interface ArtifactKind {
|
||||
converter?: string;
|
||||
}
|
||||
|
||||
type RuntimeSurfaceSourceClass = 'commands' | 'agents';
|
||||
|
||||
interface RuntimeSurfaceSourceProvider {
|
||||
kind: 'installed' | 'marker' | 'package';
|
||||
commandsRoot: string;
|
||||
agentsRoot: string;
|
||||
}
|
||||
|
||||
interface SourceResolutionContext {
|
||||
runtimeConfigDir: string;
|
||||
scope: 'local' | 'global';
|
||||
required: Set<RuntimeSurfaceSourceClass>;
|
||||
authority: 'runtime' | 'compatible';
|
||||
provider?: RuntimeSurfaceSourceProvider;
|
||||
}
|
||||
|
||||
function requiredRuntimeSurfaceSourceClasses(kinds: Iterable<{ kind?: string }>): Set<RuntimeSurfaceSourceClass> {
|
||||
const required = new Set<RuntimeSurfaceSourceClass>();
|
||||
for (const kind of kinds) {
|
||||
if (kind.kind === 'commands' || kind.kind === 'skills') required.add('commands');
|
||||
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') required.add('agents');
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
interface Layout {
|
||||
runtime: string;
|
||||
configDir: string;
|
||||
@@ -139,124 +167,283 @@ interface Layout {
|
||||
// Source root finders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Locate the GSD commands/gsd source directory.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. If runtimeConfigDir provided, check <runtimeConfigDir>/.gsd-source marker.
|
||||
* 2. Walk up from __dirname using path.dirname (no literal .. segments).
|
||||
* 3. Throw a descriptive error if neither succeeds.
|
||||
*/
|
||||
function findInstallSourceRoot(runtimeConfigDir?: string): string {
|
||||
// Step 1: marker check — reads `<runtimeConfigDir>/.gsd-source`, a path
|
||||
// under the INSTALL DESTINATION, so this probe goes through the injected
|
||||
// adapter (installFs()).
|
||||
if (runtimeConfigDir) {
|
||||
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
|
||||
if (installFs().existsSync(markerPath)) {
|
||||
try {
|
||||
const src = installFs().readFileSync(markerPath, 'utf8').trim();
|
||||
if (src && installFs().existsSync(src)) return src;
|
||||
} catch { /* fall through */ }
|
||||
function isReadableDirectory(candidate: string, routed: boolean): boolean {
|
||||
try {
|
||||
const io = routed ? installFs() : fs;
|
||||
const stat = io.lstatSync(candidate);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
||||
const entries: string[] = io.readdirSync(candidate);
|
||||
let readableFiles = 0;
|
||||
for (const name of entries) {
|
||||
const child = path.join(candidate, name);
|
||||
const childStat = io.lstatSync(child);
|
||||
if (childStat.isSymbolicLink()) return false;
|
||||
if (childStat.isDirectory()) {
|
||||
if (!isReadableDirectory(child, routed)) return false;
|
||||
readableFiles += 1;
|
||||
} else if (childStat.isFile()) {
|
||||
if (routed) io.readFileSync(child);
|
||||
else fs.accessSync(child, fs.constants.R_OK);
|
||||
readableFiles += 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return readableFiles > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: walk up from __dirname to locate the GSD PACKAGE'S OWN source
|
||||
// tree (commands/gsd/) — this resolves where the installer's own code is
|
||||
// running FROM, not anything under the install destination, so it is
|
||||
// deliberately NOT routed through the injected fs adapter (#2874): a fake
|
||||
// "destination" adapter has no reason to know about the real package's own
|
||||
// on-disk layout (an injected adapter's store starts empty and is never
|
||||
// seeded with real repo paths), and routing it through would make this
|
||||
// resolution unconditionally throw rather than gracefully staging nothing.
|
||||
//
|
||||
// Uses `fs.statSync` in a try/catch rather than `fs.existsSync` — this is
|
||||
// LOAD-BEARING, not a style choice: tests/executed-plan.test.cjs's F2 cases
|
||||
// poison every method on the ROUTED fs surface (including `existsSync`,
|
||||
// since installFs()'s REAL_ADAPTER also calls it) to prove nothing on the
|
||||
// installRuntimeArtifacts call tree reaches real fs. The F2 "nativePlugin
|
||||
// runtime: pi" test calls this function (via findInstallSourceRoot()) AFTER
|
||||
// installing that poison, specifically to resolve the pi nativePlugin
|
||||
// source path against this repo's own real layout — an operation this
|
||||
// function must still be able to perform even while `existsSync` is
|
||||
// poisoned, because this Step 2 walk is real-fs-only by design and was
|
||||
// never meant to be covered by that poison list. `statSync` is not on the
|
||||
// poisoned surface, so this probe survives; switching back to `existsSync`
|
||||
// makes that F2 test throw (verified: reverting this to `existsSync` trips
|
||||
// the poison and breaks the pi nativePlugin case).
|
||||
function isPhysicallyConfinedTo(root: string, candidate: string): boolean {
|
||||
try {
|
||||
const physicalRoot = installFs().realpathSync(root);
|
||||
const physicalCandidate = installFs().realpathSync(candidate);
|
||||
return physicalCandidate === physicalRoot || physicalCandidate.startsWith(physicalRoot + path.sep);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function installedManifestIsComplete(
|
||||
runtimeConfigDir: string,
|
||||
required: ReadonlySet<RuntimeSurfaceSourceClass>,
|
||||
): boolean {
|
||||
// This synchronous admission check binds provider selection to the corpus
|
||||
// observed here. Same-user mutation after resolution is outside #4132's
|
||||
// threat model and would require a broader snapshot/transaction design.
|
||||
const io = installFs();
|
||||
const manifestPath = path.join(runtimeConfigDir, 'gsd-file-manifest.json');
|
||||
if (!io.existsSync(manifestPath)) return false;
|
||||
if (!isPhysicallyConfinedTo(runtimeConfigDir, manifestPath)) return false;
|
||||
|
||||
try {
|
||||
const manifest = installerMigrations.readInstallManifest(runtimeConfigDir);
|
||||
if (manifest.manifestVersion === null) return false;
|
||||
const keys = Object.keys(manifest.files);
|
||||
const prefixes: string[] = [];
|
||||
if (required.has('commands')) prefixes.push('gsd-core/commands/gsd/');
|
||||
if (required.has('agents')) prefixes.push('gsd-core/agents/');
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
const expected = keys.filter((key) => key.startsWith(prefix));
|
||||
if (expected.length === 0) return false;
|
||||
const expectedSet = new Set(expected);
|
||||
const corpusRoot = path.resolve(runtimeConfigDir, ...prefix.slice(0, -1).split('/'));
|
||||
if (!isPhysicallyConfinedTo(runtimeConfigDir, corpusRoot)) return false;
|
||||
let actualFiles = 0;
|
||||
const visit = (dir: string): boolean => {
|
||||
for (const name of io.readdirSync(dir)) {
|
||||
const candidate = path.join(dir, name);
|
||||
const stat = io.lstatSync(candidate);
|
||||
if (stat.isSymbolicLink()) return false;
|
||||
if (stat.isDirectory()) {
|
||||
if (!visit(candidate)) return false;
|
||||
} else if (stat.isFile()) {
|
||||
const relative = path.relative(corpusRoot, candidate).split(path.sep).join('/');
|
||||
if (!expectedSet.has(prefix + relative)) return false;
|
||||
actualFiles += 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!visit(corpusRoot) || actualFiles !== expected.length) return false;
|
||||
for (const key of expected) {
|
||||
const parts = key.split('/');
|
||||
if (parts.some((part) => part === '' || part === '.' || part === '..')) return false;
|
||||
const candidate = path.resolve(runtimeConfigDir, ...parts);
|
||||
const root = path.resolve(runtimeConfigDir);
|
||||
if (!candidate.startsWith(root + path.sep)) return false;
|
||||
const stat = io.lstatSync(candidate);
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) return false;
|
||||
if (installerMigrations.classifyArtifact(runtimeConfigDir, key, manifest).classification !== 'managed-pristine') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function providerHasRequiredClasses(
|
||||
provider: RuntimeSurfaceSourceProvider,
|
||||
required: ReadonlySet<RuntimeSurfaceSourceClass>,
|
||||
routed: boolean,
|
||||
runtimeConfigDir?: string,
|
||||
): boolean {
|
||||
if (provider.kind === 'installed' && runtimeConfigDir) {
|
||||
return installedManifestIsComplete(runtimeConfigDir, required);
|
||||
}
|
||||
return (!required.has('commands') || isReadableDirectory(provider.commandsRoot, routed)) &&
|
||||
(!required.has('agents') || isReadableDirectory(provider.agentsRoot, routed));
|
||||
}
|
||||
|
||||
function providersShareRequiredRoots(
|
||||
left: RuntimeSurfaceSourceProvider,
|
||||
right: RuntimeSurfaceSourceProvider,
|
||||
required: ReadonlySet<RuntimeSurfaceSourceClass>,
|
||||
): boolean {
|
||||
const leftFs = left.kind === 'package' ? fs : installFs();
|
||||
const rightFs = right.kind === 'package' ? fs : installFs();
|
||||
const physicalRootsOverlap = (leftRoot: string, rightRoot: string): boolean => {
|
||||
const canonicalize = (io: typeof leftFs, root: string): string | null => {
|
||||
let existing = path.resolve(root);
|
||||
const missingSegments: string[] = [];
|
||||
while (true) {
|
||||
try {
|
||||
return path.resolve(io.realpathSync(existing), ...missingSegments);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return null;
|
||||
const parent = path.dirname(existing);
|
||||
if (parent === existing) return null;
|
||||
missingSegments.unshift(path.basename(existing));
|
||||
existing = parent;
|
||||
}
|
||||
}
|
||||
};
|
||||
const overlap = (leftPath: string, rightPath: string): boolean => {
|
||||
const relative = path.relative(leftPath, rightPath);
|
||||
return relative === '' ||
|
||||
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
||||
};
|
||||
const physicalLeft = canonicalize(leftFs, leftRoot);
|
||||
const physicalRight = canonicalize(rightFs, rightRoot);
|
||||
if (!physicalLeft || !physicalRight) return true;
|
||||
return overlap(physicalLeft, physicalRight) || overlap(physicalRight, physicalLeft);
|
||||
};
|
||||
return (required.has('commands') && physicalRootsOverlap(left.commandsRoot, right.commandsRoot)) ||
|
||||
(required.has('agents') && physicalRootsOverlap(left.agentsRoot, right.agentsRoot));
|
||||
}
|
||||
|
||||
function markerProvider(runtimeConfigDir: string): RuntimeSurfaceSourceProvider | null {
|
||||
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
|
||||
try {
|
||||
if (!installFs().existsSync(markerPath)) return null;
|
||||
const markerStat = installFs().lstatSync(markerPath);
|
||||
if (!markerStat.isFile() || markerStat.isSymbolicLink()) return null;
|
||||
const commandsRoot = installFs().readFileSync(markerPath, 'utf8').trim();
|
||||
if (!commandsRoot) return null;
|
||||
// A marker written by the installer may point at this process's executing
|
||||
// package. Preserve package-source IO on real fs so the existing injected
|
||||
// destination adapter remains destination-only (#2874).
|
||||
const packaged = packageProvider(new Set(['commands']));
|
||||
if (packaged && path.resolve(commandsRoot) === path.resolve(packaged.commandsRoot)) {
|
||||
return packaged;
|
||||
}
|
||||
return {
|
||||
kind: 'marker',
|
||||
commandsRoot,
|
||||
agentsRoot: path.resolve(path.dirname(commandsRoot), '..', 'agents'),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function packageProvider(required: ReadonlySet<RuntimeSurfaceSourceClass>): RuntimeSurfaceSourceProvider | null {
|
||||
// Package-source IO deliberately stays on real fs; injected install adapters
|
||||
// model destinations, not the executing package tree (#2874).
|
||||
let dir = __dirname;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const candidate = path.join(dir, 'commands', 'gsd');
|
||||
try {
|
||||
fs.statSync(candidate);
|
||||
return candidate;
|
||||
} catch { /* not here — keep walking up */ }
|
||||
const candidate: RuntimeSurfaceSourceProvider = {
|
||||
kind: 'package',
|
||||
commandsRoot: path.join(dir, 'commands', 'gsd'),
|
||||
agentsRoot: path.join(dir, 'agents'),
|
||||
};
|
||||
if (providerHasRequiredClasses(candidate, required, false)) return candidate;
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
|
||||
throw new Error(`findInstallSourceRoot: could not locate commands/gsd from ${__dirname}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the GSD agents source directory.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. If runtimeConfigDir provided, check <runtimeConfigDir>/.gsd-source marker.
|
||||
* 2. Walk up from __dirname using path.dirname (no literal .. segments).
|
||||
* 3. Throw a descriptive error if neither succeeds.
|
||||
* Select one complete source provider for an entire resolved layout.
|
||||
* Provider mixing is forbidden: a skills+agents layout cannot take commands
|
||||
* from one package version and agents from another.
|
||||
*/
|
||||
function findAgentsSourceRoot(runtimeConfigDir?: string): string {
|
||||
// Step 1: marker check (destination-relative — routed through installFs()).
|
||||
if (runtimeConfigDir) {
|
||||
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
|
||||
if (installFs().existsSync(markerPath)) {
|
||||
try {
|
||||
const src = installFs().readFileSync(markerPath, 'utf8').trim();
|
||||
if (src && installFs().existsSync(src)) {
|
||||
// Marker points to commands/gsd; agents/ is a sibling of commands/
|
||||
const agentsCandidate = path.resolve(path.dirname(src), '..', 'agents');
|
||||
if (installFs().existsSync(agentsCandidate)) return agentsCandidate;
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
function resolveSourceProvider(
|
||||
runtimeConfigDir: string | undefined,
|
||||
requiredClasses: Iterable<RuntimeSurfaceSourceClass>,
|
||||
scope: 'local' | 'global' = 'global',
|
||||
authority: 'runtime' | 'compatible' = 'compatible',
|
||||
): RuntimeSurfaceSourceProvider {
|
||||
const required = new Set(requiredClasses);
|
||||
let rejectedInstalled: RuntimeSurfaceSourceProvider | null = null;
|
||||
if (required.size === 0) {
|
||||
return { kind: 'package', commandsRoot: '', agentsRoot: '' };
|
||||
}
|
||||
|
||||
if (runtimeConfigDir && scope === 'global') {
|
||||
const installed: RuntimeSurfaceSourceProvider = {
|
||||
kind: 'installed',
|
||||
commandsRoot: path.join(runtimeConfigDir, 'gsd-core', 'commands', 'gsd'),
|
||||
agentsRoot: path.join(runtimeConfigDir, 'gsd-core', 'agents'),
|
||||
};
|
||||
if (providerHasRequiredClasses(installed, required, true, runtimeConfigDir)) return installed;
|
||||
rejectedInstalled = installed;
|
||||
|
||||
const marker = markerProvider(runtimeConfigDir);
|
||||
if (marker && !providersShareRequiredRoots(marker, installed, required) && providerHasRequiredClasses(marker, required, marker.kind !== 'package')) {
|
||||
return marker;
|
||||
}
|
||||
} else if (runtimeConfigDir) {
|
||||
const marker = markerProvider(runtimeConfigDir);
|
||||
if (marker && providerHasRequiredClasses(marker, required, marker.kind !== 'package')) return marker;
|
||||
}
|
||||
|
||||
if (authority === 'compatible') {
|
||||
const packaged = packageProvider(required);
|
||||
if (
|
||||
packaged &&
|
||||
(!rejectedInstalled || !providersShareRequiredRoots(packaged, rejectedInstalled, required))
|
||||
) {
|
||||
return packaged;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: walk up from __dirname — locates THIS package's own agents/
|
||||
// source tree, not the install destination. See findInstallSourceRoot's
|
||||
// Step 2 comment (#2874) for why this stays unrouted, real-fs-only, and why
|
||||
// it uses `statSync` rather than `existsSync` (load-bearing against F2's
|
||||
// poison of the routed fs surface, not a style choice).
|
||||
let dir = __dirname;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const candidate = path.join(dir, 'agents');
|
||||
try {
|
||||
fs.statSync(candidate);
|
||||
return candidate;
|
||||
} catch { /* not here — keep walking up */ }
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
throw new Error(
|
||||
`Runtime Surface source is unavailable or incomplete for ${[...required].sort().join('+')}; ` +
|
||||
'install or upgrade gsd-core before materializing this surface.',
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`findAgentsSourceRoot: could not locate agents/ from ${__dirname}`);
|
||||
function sourceRootFor(
|
||||
context: SourceResolutionContext,
|
||||
sourceClass: RuntimeSurfaceSourceClass,
|
||||
): string {
|
||||
context.provider ??= resolveSourceProvider(
|
||||
context.runtimeConfigDir,
|
||||
context.required,
|
||||
context.scope,
|
||||
context.authority,
|
||||
);
|
||||
return sourceClass === 'commands' ? context.provider.commandsRoot : context.provider.agentsRoot;
|
||||
}
|
||||
|
||||
function findInstallSourceRoot(runtimeConfigDir?: string): string {
|
||||
return resolveSourceProvider(runtimeConfigDir, ['commands']).commandsRoot;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout table builders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function commandsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
|
||||
function commandsKind(destSubpath: string, prefix: string, sourceContext: SourceResolutionContext): ArtifactKind {
|
||||
return {
|
||||
kind: 'commands',
|
||||
destSubpath,
|
||||
prefix,
|
||||
stage: (resolved) => stageSkillsForProfile(findInstallSourceRoot(configDir), resolved),
|
||||
stage: (resolved) => stageSkillsForProfile(sourceRootFor(sourceContext, 'commands'), resolved),
|
||||
};
|
||||
}
|
||||
|
||||
function agentsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
|
||||
function agentsKind(destSubpath: string, prefix: string, configDir: string, sourceContext: SourceResolutionContext): ArtifactKind {
|
||||
return {
|
||||
kind: 'agents',
|
||||
destSubpath,
|
||||
@@ -280,11 +467,11 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art
|
||||
// a caller with NO agentCtx in scope is unaffected (row I2: converter-only,
|
||||
// as today), matching stageAgentsForRuntimeWithConverter's own contract.
|
||||
stage: (resolved, agentCtx) => stageAgentsForRuntimeWithConverter(
|
||||
findAgentsSourceRoot(configDir),
|
||||
sourceRootFor(sourceContext, 'agents'),
|
||||
resolved,
|
||||
(content: string) => content,
|
||||
false,
|
||||
agentCtx,
|
||||
agentCtx?.runtime ? agentCtx : undefined,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -425,7 +612,8 @@ function convertedAgentsKind(
|
||||
prefix: string,
|
||||
converterName: string,
|
||||
configDir: string,
|
||||
scope: 'local' | 'global' = 'global',
|
||||
sourceContext: SourceResolutionContext,
|
||||
scope: 'local' | 'global',
|
||||
): ArtifactKind {
|
||||
return {
|
||||
kind: 'agents',
|
||||
@@ -501,17 +689,17 @@ function convertedAgentsKind(
|
||||
// for descriptor-driven runtimes), thread it through so stageAgentsForRuntimeWithConverter
|
||||
// can apply the full pre-converter + post-converter sequence in the correct order.
|
||||
return stageAgentsForRuntimeWithConverter(
|
||||
findAgentsSourceRoot(configDir),
|
||||
sourceRootFor(sourceContext, 'agents'),
|
||||
resolved,
|
||||
converter,
|
||||
isGlobalScope(scope),
|
||||
agentCtx,
|
||||
agentCtx?.runtime ? agentCtx : undefined,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
|
||||
function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string, sourceContext: SourceResolutionContext): ArtifactKind {
|
||||
return {
|
||||
kind: 'kimi-agents',
|
||||
destSubpath,
|
||||
@@ -527,7 +715,7 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string):
|
||||
// #2995: compose at staging (identity converter) so the readFileSync below
|
||||
// sees marker-free content — same single composing stager as agentsKind.
|
||||
const stagedAgents = stageAgentsForRuntimeWithConverter(
|
||||
findAgentsSourceRoot(configDir),
|
||||
sourceRootFor(sourceContext, 'agents'),
|
||||
resolved,
|
||||
(content: string) => content,
|
||||
false,
|
||||
@@ -586,8 +774,9 @@ function skillsKind(
|
||||
converterName: string,
|
||||
runtime: string,
|
||||
configDir: string,
|
||||
nested = false,
|
||||
scope: 'local' | 'global' = 'global',
|
||||
nested: boolean,
|
||||
scope: 'local' | 'global',
|
||||
sourceContext: SourceResolutionContext,
|
||||
capabilityRegistry?: CapabilityRegistryForSkills,
|
||||
): ArtifactKind {
|
||||
return {
|
||||
@@ -623,7 +812,7 @@ function skillsKind(
|
||||
// rewritten shape.
|
||||
const wrappedConverter = (content: string, skillName: string): string =>
|
||||
realConverter(content, skillName, runtime, cmdNames, isGlobal);
|
||||
return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested, capabilityRegistry);
|
||||
return stageSkillsForRuntimeAsSkills(sourceRootFor(sourceContext, 'commands'), resolved, wrappedConverter, prefix, nested, capabilityRegistry);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -648,7 +837,7 @@ function convertedCommandsKind(
|
||||
destSubpath: string,
|
||||
prefix: string,
|
||||
converterName: string,
|
||||
configDir: string,
|
||||
sourceContext: SourceResolutionContext,
|
||||
): ArtifactKind {
|
||||
return {
|
||||
kind: 'commands',
|
||||
@@ -656,7 +845,7 @@ function convertedCommandsKind(
|
||||
prefix,
|
||||
stage: (resolved) => {
|
||||
const converter = _resolveNamedConverter(converterName, 'commands') as (content: string, commandName: string) => string;
|
||||
return stageCommandsForRuntimeFlat(findInstallSourceRoot(configDir), resolved, converter, prefix);
|
||||
return stageCommandsForRuntimeFlat(sourceRootFor(sourceContext, 'commands'), resolved, converter, prefix);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -743,7 +932,7 @@ function getRegistry(): RegistryLike {
|
||||
* Map a single ArtifactKindDescriptor entry to an ArtifactKind using the
|
||||
* matching builder function. Mirrors the hand-built calls in the old switch.
|
||||
*/
|
||||
function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, configDir: string, scope: 'local' | 'global', capabilityRegistry?: CapabilityRegistryForSkills): ArtifactKind {
|
||||
function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, configDir: string, scope: 'local' | 'global', capabilityRegistry: CapabilityRegistryForSkills | undefined, sourceContext: SourceResolutionContext): ArtifactKind {
|
||||
const { kind, destSubpath, prefix, nesting, converter } = entry;
|
||||
const nested = nesting === 'nested';
|
||||
|
||||
@@ -751,14 +940,14 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
|
||||
switch (kind) {
|
||||
case 'commands':
|
||||
result = converter == null
|
||||
? commandsKind(destSubpath, prefix, configDir)
|
||||
: convertedCommandsKind(destSubpath, prefix, converter, configDir);
|
||||
? commandsKind(destSubpath, prefix, sourceContext)
|
||||
: convertedCommandsKind(destSubpath, prefix, converter, sourceContext);
|
||||
break;
|
||||
|
||||
case 'agents':
|
||||
result = converter == null
|
||||
? agentsKind(destSubpath, prefix, configDir)
|
||||
: convertedAgentsKind(destSubpath, prefix, converter, configDir, scope);
|
||||
? agentsKind(destSubpath, prefix, configDir, sourceContext)
|
||||
: convertedAgentsKind(destSubpath, prefix, converter, configDir, sourceContext, scope);
|
||||
break;
|
||||
|
||||
case 'skills':
|
||||
@@ -767,11 +956,11 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
|
||||
`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`,
|
||||
);
|
||||
}
|
||||
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope, capabilityRegistry);
|
||||
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope, sourceContext, capabilityRegistry);
|
||||
break;
|
||||
|
||||
case 'kimi-agents':
|
||||
result = kimiAgentsKind(destSubpath, prefix, configDir);
|
||||
result = kimiAgentsKind(destSubpath, prefix, configDir, sourceContext);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -807,7 +996,12 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
|
||||
* is not threaded in at layout-build time a `full`-profile install stages no
|
||||
* third-party capability skills regardless of registration (#2322 blocker 2).
|
||||
*/
|
||||
function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: 'local' | 'global' = 'global', capabilityRegistry?: CapabilityRegistryForSkills): Layout {
|
||||
function resolveRuntimeArtifactLayout(
|
||||
runtime: string,
|
||||
configDir: string,
|
||||
scope: 'local' | 'global' = 'global',
|
||||
capabilityRegistry?: CapabilityRegistryForSkills,
|
||||
): Layout {
|
||||
return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope, capabilityRegistry);
|
||||
}
|
||||
|
||||
@@ -831,7 +1025,20 @@ function resolveRuntimeArtifactLayoutFromRegistry(
|
||||
}
|
||||
|
||||
const entries: ArtifactKindDescriptor[] = desc[scope] ?? [];
|
||||
const kinds: ArtifactKind[] = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry));
|
||||
const required = requiredRuntimeSurfaceSourceClasses(entries);
|
||||
// Stage closures share one lazy source-resolution context, so the first kind
|
||||
// to stage selects and caches one complete provider for every required source
|
||||
// class. Global layouts accept only installed or marker providers; the
|
||||
// installer owns its private package fallback by retrying through a transient
|
||||
// compatibility marker after source resolution fails. Local layouts retain
|
||||
// compatible marker/package resolution and never provision the global corpus.
|
||||
const sourceContext: SourceResolutionContext = {
|
||||
runtimeConfigDir: configDir,
|
||||
scope,
|
||||
required,
|
||||
authority: scope === 'local' ? 'compatible' : 'runtime',
|
||||
};
|
||||
const kinds: ArtifactKind[] = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry, sourceContext));
|
||||
|
||||
return { runtime, configDir, scope, kinds };
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@ interface ApplySurfaceOptions {
|
||||
resolveAttribution?: (runtime: string) => string | null | undefined;
|
||||
homedir?: () => string;
|
||||
platform?: string;
|
||||
/** Candidate state to publish only after every artifact kind materializes. */
|
||||
surfaceState?: SurfaceState | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -235,7 +237,7 @@ function normalizeSkillManifest(runtimeConfigDir: string, manifest: Map<string,
|
||||
* - registry is threaded into resolveProfile so capability skills
|
||||
* participate in the base skill set and their requires: chains expand.
|
||||
*/
|
||||
function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]> | object, clusterMap?: ClusterMap | Record<string, string[]>, registry?: { capabilityClusters?: Record<string, string[]>; profileMembership?: Record<string, { tier: string; profiles: string[] }> }): { name: string; skills: Set<string>; agents: Set<string> } {
|
||||
function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]> | object, clusterMap?: ClusterMap | Record<string, string[]>, registry?: { capabilityClusters?: Record<string, string[]>; profileMembership?: Record<string, { tier: string; profiles: string[] }> }, surfaceOverride?: SurfaceState | null): { name: string; skills: Set<string>; agents: Set<string> } {
|
||||
// Merge capability clusters into the cluster map when registry is provided.
|
||||
// The ADR-857 phase 4a HARD gate guarantees that when a capId matches a CLUSTERS
|
||||
// key, the values are EQUAL — so the spread is idempotent for matching names.
|
||||
@@ -269,7 +271,7 @@ function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]
|
||||
cm = merged;
|
||||
}
|
||||
const skillManifest = normalizeSkillManifest(runtimeConfigDir, manifest);
|
||||
const surface = readSurface(runtimeConfigDir);
|
||||
const surface = surfaceOverride === undefined ? readSurface(runtimeConfigDir) : surfaceOverride;
|
||||
|
||||
// Determine base profile name: from surface state or from .gsd-profile marker
|
||||
const baseProfileName = (surface && surface.baseProfile)
|
||||
@@ -382,10 +384,9 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
os: deps.os, env: deps.env,
|
||||
});
|
||||
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
|
||||
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
|
||||
// Profile toggles must converge retired surfaces too. Once a kind disappears
|
||||
// from artifactLayout there is no normal sync pass left to prune it (#2644).
|
||||
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(layout.runtime, layout.configDir);
|
||||
const hasCandidateState = opts !== undefined && Object.prototype.hasOwnProperty.call(opts, 'surfaceState');
|
||||
const candidateState = hasCandidateState ? (opts.surfaceState ?? null) : undefined;
|
||||
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry, candidateState);
|
||||
// #1575: agents kind now mirrors createRuntimeArtifactInstallPlan — build
|
||||
// agentCtx (pathPrefix + attribution) and pass it to kind.stage() so
|
||||
// stageAgentsForRuntimeWithConverter applies the full inline-loop pipeline
|
||||
@@ -420,7 +421,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
// not referenced by any skill's _calls_agents_ manifest entry would be silently
|
||||
// dropped from the surface path. For tiered profiles (core/standard) or when
|
||||
// surface mods exist, pass the resolved set so only the filtered subset stages.
|
||||
const _surfaceState = readSurface(layout.configDir);
|
||||
const _surfaceState = candidateState === undefined ? readSurface(layout.configDir) : candidateState;
|
||||
const _baseProfileName = (_surfaceState && _surfaceState.baseProfile)
|
||||
? _surfaceState.baseProfile
|
||||
: (readActiveProfile(layout.configDir) || 'full');
|
||||
@@ -430,6 +431,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
_surfaceState.explicitRemoves.length > 0
|
||||
);
|
||||
const _isUnmodifiedFull = _baseProfileName === 'full' && !_hasSurfaceMods;
|
||||
const stagedKinds: { kind: ArtifactKind; staged: string; dest: string }[] = [];
|
||||
try {
|
||||
for (const kind of layout.kinds) {
|
||||
let staged: string;
|
||||
@@ -463,7 +465,23 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
// the parity test in tests/runtime-artifact-layout-surface.test.cjs
|
||||
// enforces that the two writers never diverge again.
|
||||
const dest = assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath);
|
||||
_syncGsdDir(staged, dest, kind, skillManifest, layout.runtime);
|
||||
stagedKinds.push({ kind, staged, dest });
|
||||
}
|
||||
|
||||
// Do not mutate installed artifacts until every kind has staged
|
||||
// successfully. A missing source provider therefore leaves both artifacts
|
||||
// and the persisted surface state untouched.
|
||||
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(layout.runtime, layout.configDir);
|
||||
for (const item of stagedKinds) {
|
||||
_syncGsdDir(item.staged, item.dest, item.kind, skillManifest, layout.runtime);
|
||||
}
|
||||
|
||||
if (hasCandidateState) {
|
||||
if (candidateState === null) {
|
||||
fs.rmSync(path.join(runtimeConfigDir, SURFACE_FILE_NAME), { force: true });
|
||||
} else if (candidateState !== undefined) {
|
||||
writeSurface(runtimeConfigDir, candidateState);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
for (const dir of tempDirsToClean) {
|
||||
|
||||
Reference in New Issue
Block a user