@kuznai/inception-engine 0.25.0 → 1.0.0

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.
@@ -186,7 +186,18 @@ export async function executeRevert(actions, dryRun, verbose, home, deps = {}, s
186
186
  registry: runRegistry,
187
187
  };
188
188
  if (!dryRun) {
189
- await runRegistry.preflight(home);
189
+ try {
190
+ await runRegistry.preflight(home);
191
+ }
192
+ catch (err) {
193
+ const message = err instanceof Error ? err.message : String(err);
194
+ return {
195
+ succeeded: 0,
196
+ skipped: 0,
197
+ failed: actions.map((action) => ({ action, error: message })),
198
+ planned,
199
+ };
200
+ }
190
201
  }
191
202
  for (const action of actions) {
192
203
  if (signal?.aborted)
@@ -1,6 +1,18 @@
1
1
  import type { AgentId } from "../schemas/manifest.ts";
2
2
  export declare function sourceAccessError(err: unknown, sourcePath: string): string;
3
3
  export declare function validateSourcePath(source: string, skillPath: string, resolvedSourceDir: string, realRoot: string): Promise<void>;
4
+ export type SourcePathValidator = (source: string, manifestPath: string, resolvedSourceDir: string) => Promise<void>;
5
+ /**
6
+ * Returns a memoizing source-path validator bound to a single `realRoot`.
7
+ *
8
+ * The cheap out-of-root string gate still runs on every call so per-entry
9
+ * `manifestPath` error messages remain accurate. The realpath resolution and
10
+ * the identity-based ancestor walk are memoized per resolved `source`, which
11
+ * eliminates repeated syscalls when the same source is referenced across
12
+ * multiple manifest sections or fans out to multiple agents within one
13
+ * `planDeploy` invocation.
14
+ */
15
+ export declare function createSourcePathValidator(realRoot: string): SourcePathValidator;
4
16
  export declare function validateSourceFile(sourcePath: string, manifestPath: string): Promise<void>;
5
17
  export declare function validateMcpServerConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
6
18
  export declare function validatePermissionsConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
@@ -11,7 +23,23 @@ export declare function validateSkillDefinitionFile(sourcePath: string, manifest
11
23
  body: string;
12
24
  }>;
13
25
  /**
14
- * Validates that an instruction file (agentRules or agentDefinitions) meets
15
- * the structural requirements of the target agent.
26
+ * Returns true when the agent requires its instruction-file source to expose
27
+ * structural YAML frontmatter. Used by adapters to decide whether to parse
28
+ * the source document before per-agent validation.
29
+ */
30
+ export declare function instructionRequiresFrontmatter(agentId: AgentId): boolean;
31
+ /**
32
+ * Reads and parses an instruction file's YAML frontmatter once. Callers that
33
+ * fan out to multiple target agents should invoke this at most once per
34
+ * source file and reuse the returned attributes for each
35
+ * `validateInstructionAgentRequirements` call.
36
+ */
37
+ export declare function parseInstructionDocument(sourcePath: string, manifestPath: string): Promise<{
38
+ attributes: Record<string, unknown>;
39
+ body: string;
40
+ }>;
41
+ /**
42
+ * Runs the per-agent structural checks against an already-parsed instruction
43
+ * document. Agents without frontmatter requirements are a no-op.
16
44
  */
17
- export declare function validateInstructionFileRequirements(sourcePath: string, manifestPath: string, agentId: AgentId): Promise<void>;
45
+ export declare function validateInstructionAgentRequirements(attributes: Record<string, unknown>, manifestPath: string, agentId: AgentId): void;
@@ -54,6 +54,47 @@ export async function validateSourcePath(source, skillPath, resolvedSourceDir, r
54
54
  // Source doesn't exist yet — will be caught during execute
55
55
  }
56
56
  }
57
+ /**
58
+ * Returns a memoizing source-path validator bound to a single `realRoot`.
59
+ *
60
+ * The cheap out-of-root string gate still runs on every call so per-entry
61
+ * `manifestPath` error messages remain accurate. The realpath resolution and
62
+ * the identity-based ancestor walk are memoized per resolved `source`, which
63
+ * eliminates repeated syscalls when the same source is referenced across
64
+ * multiple manifest sections or fans out to multiple agents within one
65
+ * `planDeploy` invocation.
66
+ */
67
+ export function createSourcePathValidator(realRoot) {
68
+ const cache = new Map();
69
+ const resolveOutcome = async (source) => {
70
+ try {
71
+ const realSource = await realpath(source);
72
+ if (isSameOrDescendantPath(realSource, realRoot) ||
73
+ (await isWithinRootByIdentity(realSource, realRoot))) {
74
+ return { kind: "ok" };
75
+ }
76
+ return { kind: "escape", realSource };
77
+ }
78
+ catch {
79
+ // Source doesn't exist yet — will be caught during execute
80
+ return { kind: "ok" };
81
+ }
82
+ };
83
+ return async (source, manifestPath, resolvedSourceDir) => {
84
+ if (!source.startsWith(resolvedSourceDir + path.sep)) {
85
+ throw new UserError("DEPLOY_FAILED", `Skill path "${manifestPath}" resolves outside the repository root: ${source}`);
86
+ }
87
+ let pending = cache.get(source);
88
+ if (!pending) {
89
+ pending = resolveOutcome(source);
90
+ cache.set(source, pending);
91
+ }
92
+ const outcome = await pending;
93
+ if (outcome.kind === "escape") {
94
+ throw new UserError("DEPLOY_FAILED", `Skill path "${manifestPath}" resolves outside the repository root via symlink: ${source} -> ${outcome.realSource}`);
95
+ }
96
+ };
97
+ }
57
98
  export async function validateSourceFile(sourcePath, manifestPath) {
58
99
  let stat;
59
100
  try {
@@ -308,24 +349,37 @@ function validateAntigravityRequirements(attributes, manifestPath) {
308
349
  }
309
350
  }
310
351
  /**
311
- * Validates that an instruction file (agentRules or agentDefinitions) meets
312
- * the structural requirements of the target agent.
352
+ * Returns true when the agent requires its instruction-file source to expose
353
+ * structural YAML frontmatter. Used by adapters to decide whether to parse
354
+ * the source document before per-agent validation.
313
355
  */
314
- export async function validateInstructionFileRequirements(sourcePath, manifestPath, agentId) {
315
- const requiresFrontmatter = AGENT_REGISTRY_BY_ID[agentId]?.instructionFrontmatterRequired === true;
316
- if (!requiresFrontmatter)
317
- return;
318
- let attributes;
356
+ export function instructionRequiresFrontmatter(agentId) {
357
+ return AGENT_REGISTRY_BY_ID[agentId]?.instructionFrontmatterRequired === true;
358
+ }
359
+ /**
360
+ * Reads and parses an instruction file's YAML frontmatter once. Callers that
361
+ * fan out to multiple target agents should invoke this at most once per
362
+ * source file and reuse the returned attributes for each
363
+ * `validateInstructionAgentRequirements` call.
364
+ */
365
+ export async function parseInstructionDocument(sourcePath, manifestPath) {
319
366
  try {
320
- const result = await validateSkillDefinitionFile(sourcePath, manifestPath);
321
- attributes = result.attributes;
367
+ return await validateSkillDefinitionFile(sourcePath, manifestPath);
322
368
  }
323
369
  catch (err) {
324
370
  if (err instanceof UserError) {
325
- throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "${agentId}" failed structural validation: ${err.message}`);
371
+ throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" failed structural validation: ${err.message}`, { cause: err });
326
372
  }
327
373
  throw err;
328
374
  }
375
+ }
376
+ /**
377
+ * Runs the per-agent structural checks against an already-parsed instruction
378
+ * document. Agents without frontmatter requirements are a no-op.
379
+ */
380
+ export function validateInstructionAgentRequirements(attributes, manifestPath, agentId) {
381
+ if (!instructionRequiresFrontmatter(agentId))
382
+ return;
329
383
  if (agentId === "github-copilot") {
330
384
  validateGithubCopilotRequirements(attributes, manifestPath);
331
385
  }
@@ -55,7 +55,7 @@ const targetTemplateField = z
55
55
  .string({ message: "target must be a non-empty string" })
56
56
  .min(1, { message: "target must be a non-empty string" })
57
57
  .refine((t) => TARGET_TEMPLATE_RE.test(t), {
58
- message: "target must start with a known placeholder: {home}, {appdata}, {xdg_config}, or {repo}",
58
+ message: "target must start with a known placeholder: {home}, {appdata}, {xdg_config}, {repo}, or {workspace}",
59
59
  })
60
60
  .refine((t) => !t.split(/[\\/]+/).includes(".."), {
61
61
  message: "target must not escape its placeholder root",
@@ -120,7 +120,7 @@ export interface AgentConfig {
120
120
  unsupportedSurfaces?: AgentSurfaceSupport[];
121
121
  /**
122
122
  * When true, instruction files deployed to this agent must include valid
123
- * YAML frontmatter. Drives validateInstructionFileRequirements without
123
+ * YAML frontmatter. Drives instructionRequiresFrontmatter without
124
124
  * hardcoded agent-ID checks.
125
125
  */
126
126
  instructionFrontmatterRequired?: boolean;
@@ -1,9 +1,9 @@
1
1
  import assert from "node:assert/strict";
2
- import { chmod, mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { describe, it } from "node:test";
5
5
  import { executeDeploy, planDeploy } from "../../../src/core/deploy.js";
6
- import { lookupDeployment, registerDeployment, } from "../../../src/core/ownership.js";
6
+ import { defaultRegistryPersistence, lookupDeployment, registerDeployment, } from "../../../src/core/ownership.js";
7
7
  import { UserError } from "../../../src/errors.js";
8
8
  import { exists, makeTmpDir } from "../../helpers/fs.js";
9
9
  import { createSkillSource, testSkillManifest, } from "../../helpers/skill-dir.js";
@@ -215,9 +215,16 @@ describe("atomic redeploy behavior (Windows)", {
215
215
  const backupPath = `${target}.inception-backup`;
216
216
  const { succeeded: firstSucceeded } = await executeDeploy(actions, false, false, home);
217
217
  assert.equal(firstSucceeded, 1);
218
- const registryFile = path.join(home, ".inception-engine", "registry.json");
219
- await chmod(registryFile, 0o444);
220
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
218
+ // Simulate an unwritable registry via a failing RegistryPersistence
219
+ // instead of relying on chmod, which is not enforced for admin processes
220
+ // on Windows (e.g. GitHub Actions windows-latest runners).
221
+ const failingRegistry = {
222
+ load: (h) => defaultRegistryPersistence.load(h),
223
+ save: async () => {
224
+ throw Object.assign(new Error("EACCES: permission denied, open 'registry.json'"), { code: "EACCES" });
225
+ },
226
+ };
227
+ const { succeeded, failed } = await executeDeploy(actions, false, false, home, { registry: failingRegistry });
221
228
  assert.equal(succeeded, 0);
222
229
  assert.equal(failed.length, 1);
223
230
  assert.ok(!(await exists(backupPath)));
@@ -225,12 +232,6 @@ describe("atomic redeploy behavior (Windows)", {
225
232
  assert.ok(await exists(path.join(target, "SKILL.md")));
226
233
  }
227
234
  finally {
228
- try {
229
- await chmod(path.join(home, ".inception-engine", "registry.json"), 0o666);
230
- }
231
- catch {
232
- // best effort
233
- }
234
235
  await rm(sourceDir, { recursive: true, force: true });
235
236
  await rm(home, { recursive: true, force: true });
236
237
  }