@kuznai/inception-engine 0.14.1 → 0.16.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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ GitHub Copilot is no longer treated as a separate instruction or skill target in
8
8
 
9
9
  The broader portability layer is the roadmap direction, but this README focuses on what is working now.
10
10
 
11
- `init` is available as a bootstrap command, but what works today is intentionally narrow: it scans for directories containing `SKILL.md` and generates starter `skills` entries plus empty `mcpServers` and `agentRules` arrays. It does not infer `files`, `configs`, MCP server definitions, or rules files from the repository yet.
11
+ `init` is available as a bootstrap command. It scans for directories containing `SKILL.md`, discovers agent-rules Markdown files using the Claude-first portability conventions, and reads `mcp-servers.json` from the repo root to populate `mcpServers`. `files` and `configs` remain empty `init` emits guidance when it detects a `files/` or `configs/` directory.
12
12
 
13
13
  ## Quick Start
14
14
 
@@ -50,7 +50,8 @@ Managed skills overwrite their previous version. If a target exists but was not
50
50
  | Config patch (JSON merge) | All agents via manifest and CLI | All agents |
51
51
  | MCP Servers | claude-code, gemini-cli, codex, antigravity, opencode; github-copilot repo-scoped surfaces are warned and skipped | claude-code, gemini-cli, codex, antigravity, opencode |
52
52
  | Global/Repo Rules Files | All agents (antigravity uses repo-local `.agents/rules/`); github-copilot reads CLAUDE.md natively (deploy via claude-code) | All agents |
53
- | `init` manifest generation | Scans `SKILL.md` directories and writes starter `skills` entries | N/A |
53
+ | Permissions / Approval Config | claude-code (`~/.claude/settings.json`), codex (`~/.codex/config.toml`); other agents are warned and skipped | claude-code, codex |
54
+ | `init` manifest generation | Scans `SKILL.md` directories (`skills`), `.md` files with Claude-first agent mapping (`agentRules`), and `mcp-servers.json` (`mcpServers`); emits hints for `files/` and `configs/` directories | N/A |
54
55
 
55
56
  Features that depend on agent-specific config surfaces are intentionally conservative: if a target path or schema is not implemented with enough confidence, inception-engine warns and skips it rather than guessing.
56
57
 
@@ -98,6 +99,18 @@ Create an `inception.json` file at the root of your skills directory:
98
99
  "path": "rules/CLAUDE.md",
99
100
  "agents": ["claude-code"]
100
101
  }
102
+ ],
103
+ "permissions": [
104
+ {
105
+ "name": "safety-defaults",
106
+ "agents": ["claude-code"],
107
+ "config": {
108
+ "permissions": {
109
+ "allow": ["Read", "Glob"],
110
+ "deny": ["Bash(rm:*)"]
111
+ }
112
+ }
113
+ }
101
114
  ]
102
115
  }
103
116
  ```
@@ -145,6 +158,47 @@ Each **agentRules** entry deploys a Markdown instruction file to an agent's supp
145
158
 
146
159
  Instruction rule deployment is supported for all agents. For most agents, this targets a single global rules file (e.g., `~/.claude/CLAUDE.md` for `claude-code`, `~/.codex/AGENTS.md` for `codex`, `~/.gemini/GEMINI.md` for `gemini-cli`, and `~/.config/opencode/AGENTS.md` for `opencode`). For `antigravity`, inception-engine deploys to repo-local instruction surfaces at `{repo}/.agents/rules/{name}.md`. For `github-copilot`, no separate deployment is needed because Copilot reads `CLAUDE.md` natively — target it via the `claude-code` agentRules entry and it reaches Copilot automatically. Revert removes the deployed rules file.
147
160
 
161
+ Each **permissions** entry deploys execution and safety-oriented configuration to an agent's permission or approval surface:
162
+
163
+ - **name** - Unique identifier (same format as skill names)
164
+ - **agents** - Array of agent IDs to deploy this entry to
165
+ - **config** - Permission config payload; shape is validated per agent (see below)
166
+
167
+ Permission deployment is currently supported for `claude-code` and `codex`. Other agents emit a warning and are skipped.
168
+
169
+ For `claude-code`, the config is merged into `~/.claude/settings.json`. Only the `permissions` key is accepted:
170
+
171
+ ```json
172
+ {
173
+ "name": "safety-defaults",
174
+ "agents": ["claude-code"],
175
+ "config": {
176
+ "permissions": {
177
+ "allow": ["Read", "Glob", "Bash(git:*)"],
178
+ "deny": ["Bash(rm:*)"]
179
+ }
180
+ }
181
+ }
182
+ ```
183
+
184
+ Both `allow` and `deny` are optional string arrays. Tool patterns follow Claude Code's permission glob syntax (e.g., `Bash(git:*)` to allow all git commands).
185
+
186
+ For `codex`, the config is merged into `~/.codex/config.toml`. Only the `approval_policy` key is accepted:
187
+
188
+ ```json
189
+ {
190
+ "name": "codex-approval",
191
+ "agents": ["codex"],
192
+ "config": {
193
+ "approval_policy": "suggest"
194
+ }
195
+ }
196
+ ```
197
+
198
+ Valid `approval_policy` values are `"auto"`, `"manual"`, `"suggest"`, and `"on-failure"`.
199
+
200
+ Revert restores the previous config values using the undo patch recorded at deploy time.
201
+
148
202
  ## Creating Skills
149
203
 
150
204
  Each skill is a directory containing at minimum a `SKILL.md` file with YAML frontmatter:
@@ -164,7 +218,7 @@ The `name` and `description` fields in the frontmatter are used by most agents.
164
218
 
165
219
  ## `init` Command
166
220
 
167
- `init` is meant to bootstrap a repository that already has skill folders. It recursively scans the target directory, treats any directory containing `SKILL.md` as a skill, and writes a starter `inception.json`.
221
+ `init` is meant to bootstrap a repository that already has skills and related manifest assets. It recursively scans the target directory, treats any directory containing `SKILL.md` as a skill, discovers supported instruction and MCP conventions, and writes a starter `inception.json`.
168
222
 
169
223
  Current `init` behavior:
170
224
 
@@ -173,12 +227,58 @@ Current `init` behavior:
173
227
  - Applies either the `--agents` list or all currently known agent IDs
174
228
  - Refuses to overwrite an existing `inception.json` unless `--force` is provided
175
229
  - Supports `--dry-run` so you can inspect the generated manifest before writing it
230
+ - Discovers agent-rules Markdown files in the root and conventional subdirectories (`rules/`, `instructions/`, `.github/`, `.agents/rules/`), mapping them to agents using Claude-first portability conventions: `copilot-instructions.md` maps to `claude-code` (Copilot reads `CLAUDE.md` natively), and the fallback for unrecognized files excludes agents whose agentRules surface is unsupported
231
+ - Reads `mcp-servers.json` from the repo root (if present) and generates `mcpServers` entries; invalid entries are warned and skipped
232
+ - Reads `files-manifest.json` from the repo root (if present) and generates `files` entries; invalid entries are warned and skipped
233
+ - Reads `configs-manifest.json` from the repo root (if present) and generates `configs` entries; invalid entries are warned and skipped
234
+ - Emits guidance to create a sidecar manifest when a `files/` or `configs/` directory is detected but the corresponding sidecar file is absent
176
235
 
177
236
  Current `init` limitations:
178
237
 
179
238
  - It does not reconcile generated manifest entries against `SKILL.md` frontmatter values
180
- - It does not infer `files`, `configs`, `mcpServers`, or `agentRules`
181
- - It does not reconcile generated output with the longer-term Claude-first portability direction
239
+ - `files` and `configs` target paths are deployment-system-specific and cannot be inferred; they must be provided explicitly in `files-manifest.json` and `configs-manifest.json`
240
+
241
+ ### Repo Conventions Recognized by `init`
242
+
243
+ Place a `mcp-servers.json` file at the repo root to have `init` populate the `mcpServers` section automatically. The file must be a JSON array of MCP server entries using the same schema as the `mcpServers` field in `inception.json`:
244
+
245
+ ```json
246
+ [
247
+ {
248
+ "name": "my-server",
249
+ "agents": ["claude-code", "gemini-cli"],
250
+ "config": { "command": "npx", "args": ["-y", "my-mcp-server"] }
251
+ }
252
+ ]
253
+ ```
254
+
255
+ Place a `files-manifest.json` file at the repo root to have `init` populate the `files` section automatically. The file must be a JSON array of file entries using the same schema as the `files` field in `inception.json`:
256
+
257
+ ```json
258
+ [
259
+ {
260
+ "name": "my-settings",
261
+ "path": "files/settings.json",
262
+ "target": "{home}/.claude/settings.json",
263
+ "agents": ["claude-code"]
264
+ }
265
+ ]
266
+ ```
267
+
268
+ Place a `configs-manifest.json` file at the repo root to have `init` populate the `configs` section automatically. The file must be a JSON array of config entries using the same schema as the `configs` field in `inception.json`:
269
+
270
+ ```json
271
+ [
272
+ {
273
+ "name": "enable-feature",
274
+ "target": "{home}/.claude/settings.json",
275
+ "patch": { "someFeature": true },
276
+ "agents": ["claude-code"]
277
+ }
278
+ ]
279
+ ```
280
+
281
+ Invalid entries are warned and skipped; the rest are written into the generated manifest verbatim.
182
282
 
183
283
  ## CLI Reference
184
284
 
@@ -17,6 +17,7 @@ export const AGENT_REGISTRY = [
17
17
  detectBinary: "documented",
18
18
  mcpConfig: "documented",
19
19
  agentRules: "documented",
20
+ permissions: "documented",
20
21
  },
21
22
  mcpSupport: {
22
23
  status: "supported",
@@ -34,6 +35,14 @@ export const AGENT_REGISTRY = [
34
35
  windows: ["{home}", ".claude", "CLAUDE.md"],
35
36
  },
36
37
  },
38
+ permissionsSupport: {
39
+ status: "supported",
40
+ schemaLabel: "JSON permissions config",
41
+ path: {
42
+ posix: ["{home}", ".claude", "settings.json"],
43
+ windows: ["{home}", ".claude", "settings.json"],
44
+ },
45
+ },
37
46
  },
38
47
  {
39
48
  id: "codex",
@@ -52,6 +61,7 @@ export const AGENT_REGISTRY = [
52
61
  detectPaths: "documented",
53
62
  detectBinary: "documented",
54
63
  agentRules: "documented",
64
+ permissions: "documented",
55
65
  },
56
66
  mcpSupport: {
57
67
  status: "supported",
@@ -69,6 +79,14 @@ export const AGENT_REGISTRY = [
69
79
  windows: ["{home}", ".codex", "AGENTS.md"],
70
80
  },
71
81
  },
82
+ permissionsSupport: {
83
+ status: "supported",
84
+ schemaLabel: "TOML approval policy config",
85
+ path: {
86
+ posix: ["{home}", ".codex", "config.toml"],
87
+ windows: ["{home}", ".codex", "config.toml"],
88
+ },
89
+ },
72
90
  },
73
91
  {
74
92
  id: "gemini-cli",
@@ -105,6 +123,11 @@ export const AGENT_REGISTRY = [
105
123
  windows: ["{home}", ".gemini", "GEMINI.md"],
106
124
  },
107
125
  },
126
+ permissionsSupport: {
127
+ status: "unsupported",
128
+ schemaLabel: "global permissions surface",
129
+ reason: "Gemini CLI does not expose a documented global per-user permission or approval config surface",
130
+ },
108
131
  },
109
132
  {
110
133
  id: "antigravity",
@@ -139,6 +162,11 @@ export const AGENT_REGISTRY = [
139
162
  windows: ["{repo}", ".agents", "rules", "{name}.md"],
140
163
  },
141
164
  },
165
+ permissionsSupport: {
166
+ status: "unsupported",
167
+ schemaLabel: "global permissions surface",
168
+ reason: "Antigravity does not expose a documented global per-user permission or approval config surface",
169
+ },
142
170
  },
143
171
  {
144
172
  id: "opencode",
@@ -175,6 +203,11 @@ export const AGENT_REGISTRY = [
175
203
  windows: ["{appdata}", "opencode", "AGENTS.md"],
176
204
  },
177
205
  },
206
+ permissionsSupport: {
207
+ status: "unsupported",
208
+ schemaLabel: "global permissions surface",
209
+ reason: "OpenCode does not expose a documented global per-user permission or approval config surface",
210
+ },
178
211
  },
179
212
  {
180
213
  id: "github-copilot",
@@ -203,6 +236,11 @@ export const AGENT_REGISTRY = [
203
236
  schemaLabel: "Claude-native shared instructions",
204
237
  reason: 'GitHub Copilot reads CLAUDE.md natively, so deploy via the "claude-code" agentRules target instead of a separate rules surface',
205
238
  },
239
+ permissionsSupport: {
240
+ status: "unsupported",
241
+ schemaLabel: "global permissions surface",
242
+ reason: "GitHub Copilot permissions are managed via organization policy, not a deployable per-user config surface",
243
+ },
206
244
  policyNote: "Organization policies may override locally deployed skills. Verify with your GitHub org admin if deployed skills are not active.",
207
245
  },
208
246
  ];
@@ -1,11 +1,12 @@
1
- import type { AgentRuleEntry, McpServerEntry } from "../../schemas/manifest.ts";
1
+ import type { AgentRuleEntry, McpServerEntry, PermissionsEntry } from "../../schemas/manifest.ts";
2
2
  import type { AgentId, ConfigPatchDeployAction, FileWriteDeployAction, FrontmatterEmitDeployAction, PlanWarning, TomlPatchDeployAction } from "../../types.ts";
3
3
  import { compileMcpServerReverts } from "./mcp.ts";
4
+ import { compilePermissionsReverts } from "./permissions.ts";
4
5
  import { compileAgentRuleReverts } from "./rules.ts";
5
- export { compileAgentRuleReverts, compileMcpServerReverts };
6
+ export { compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, };
6
7
  export type AdapterAction = ConfigPatchDeployAction | FileWriteDeployAction | TomlPatchDeployAction | FrontmatterEmitDeployAction;
7
8
  export interface AdapterResult {
8
9
  actions: AdapterAction[];
9
10
  warnings: PlanWarning[];
10
11
  }
11
- export declare function compileAdapterActions(mcpServers: McpServerEntry[], agentRules: AgentRuleEntry[], sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string, repo?: string): Promise<AdapterResult>;
12
+ export declare function compileAdapterActions(mcpServers: McpServerEntry[], agentRules: AgentRuleEntry[], permissions: PermissionsEntry[], sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string, repo?: string): Promise<AdapterResult>;
@@ -1,7 +1,8 @@
1
1
  import { compileMcpServerActions, compileMcpServerReverts } from "./mcp.js";
2
+ import { compilePermissionsActions, compilePermissionsReverts, } from "./permissions.js";
2
3
  import { compileAgentRuleActions, compileAgentRuleReverts } from "./rules.js";
3
- export { compileAgentRuleReverts, compileMcpServerReverts };
4
- export async function compileAdapterActions(mcpServers, agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo) {
4
+ export { compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, };
5
+ export async function compileAdapterActions(mcpServers, agentRules, permissions, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo) {
5
6
  const actions = [];
6
7
  const warnings = [];
7
8
  for (const entry of mcpServers) {
@@ -14,5 +15,10 @@ export async function compileAdapterActions(mcpServers, agentRules, sourceDir, r
14
15
  actions.push(...r.actions);
15
16
  warnings.push(...r.warnings);
16
17
  }
18
+ for (const entry of permissions) {
19
+ const r = compilePermissionsActions(entry, detectedAgents, home);
20
+ actions.push(...r.actions);
21
+ warnings.push(...r.warnings);
22
+ }
17
23
  return { actions, warnings };
18
24
  }
@@ -0,0 +1,8 @@
1
+ import type { PermissionsEntry } from "../../schemas/manifest.ts";
2
+ import type { AgentId, ConfigPatchDeployAction, ConfigPatchRevertAction, PlanWarning, TomlPatchDeployAction, TomlPatchRevertAction } from "../../types.ts";
3
+ export interface PermissionsAdapterResult {
4
+ actions: Array<ConfigPatchDeployAction | TomlPatchDeployAction>;
5
+ warnings: PlanWarning[];
6
+ }
7
+ export declare function compilePermissionsActions(entry: PermissionsEntry, detectedAgents: AgentId[], home: string): PermissionsAdapterResult;
8
+ export declare function compilePermissionsReverts(entry: PermissionsEntry, agentFilter: AgentId[] | null, home: string): Array<ConfigPatchRevertAction | TomlPatchRevertAction>;
@@ -0,0 +1,81 @@
1
+ import path from "node:path";
2
+ import { AGENT_REGISTRY_BY_ID } from "../../config/agents.js";
3
+ import { getPlatformKey, resolvePlaceholders } from "../resolve.js";
4
+ import { validatePermissionsConfigShape } from "../validation.js";
5
+ function isTomlTarget(targetPath) {
6
+ return path.extname(targetPath).toLowerCase() === ".toml";
7
+ }
8
+ export function compilePermissionsActions(entry, detectedAgents, home) {
9
+ const actions = [];
10
+ const warnings = [];
11
+ const platform = getPlatformKey();
12
+ for (const agentId of entry.agents) {
13
+ if (!detectedAgents.includes(agentId))
14
+ continue;
15
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
16
+ const support = agent?.permissionsSupport;
17
+ if (!support || support.status === "unsupported") {
18
+ warnings.push({
19
+ kind: "confidence",
20
+ message: `permissions: agent "${agentId}" ${support?.status === "unsupported" ? support.reason : "does not expose a supported permissions adapter"} — skipping "${entry.name}"`,
21
+ });
22
+ continue;
23
+ }
24
+ validatePermissionsConfigShape(entry.config, entry.name, agentId);
25
+ const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home);
26
+ const resolvedTarget = path.resolve(rawTarget);
27
+ const confidence = agent.provenance.permissions ?? "provisional";
28
+ if (isTomlTarget(resolvedTarget)) {
29
+ actions.push({
30
+ kind: "toml-patch",
31
+ skill: entry.name,
32
+ agent: agentId,
33
+ target: resolvedTarget,
34
+ config: entry.config,
35
+ confidence,
36
+ });
37
+ }
38
+ else {
39
+ actions.push({
40
+ kind: "config-patch",
41
+ skill: entry.name,
42
+ agent: agentId,
43
+ target: resolvedTarget,
44
+ patch: entry.config,
45
+ confidence,
46
+ });
47
+ }
48
+ }
49
+ return { actions, warnings };
50
+ }
51
+ export function compilePermissionsReverts(entry, agentFilter, home) {
52
+ const actions = [];
53
+ const platform = getPlatformKey();
54
+ for (const agentId of entry.agents) {
55
+ if (agentFilter && !agentFilter.includes(agentId))
56
+ continue;
57
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
58
+ const support = agent?.permissionsSupport;
59
+ if (!support || support.status === "unsupported")
60
+ continue;
61
+ const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home);
62
+ const target = path.resolve(rawTarget);
63
+ if (isTomlTarget(target)) {
64
+ actions.push({
65
+ kind: "toml-patch",
66
+ skill: entry.name,
67
+ agent: agentId,
68
+ target,
69
+ });
70
+ }
71
+ else {
72
+ actions.push({
73
+ kind: "config-patch",
74
+ skill: entry.name,
75
+ agent: agentId,
76
+ target,
77
+ });
78
+ }
79
+ }
80
+ return actions;
81
+ }
@@ -192,7 +192,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
192
192
  ...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
193
193
  ...planConfigPatchActions(manifest, detectedAgents, home),
194
194
  ];
195
- const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
195
+ const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
196
196
  actions.push(...adapterResult.actions);
197
197
  const warnings = [
198
198
  ...detectAmbiguities(detectedAgents, manifest),
package/dist/core/init.js CHANGED
@@ -1,7 +1,8 @@
1
- import { access, readdir, writeFile } from "node:fs/promises";
1
+ import { access, readFile, readdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
3
4
  import { dryRunPrefix, logger } from "../logger.js";
4
- import { AGENT_IDS } from "../schemas/manifest.js";
5
+ import { AGENT_IDS, ConfigEntrySchema, FileEntrySchema, McpServerEntrySchema, } from "../schemas/manifest.js";
5
6
  const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
6
7
  // Ordered list: first match wins. Catch-all is applied at call site.
7
8
  const AGENT_RULES_FILE_PATTERNS = [
@@ -17,7 +18,7 @@ const AGENT_RULES_FILE_PATTERNS = [
17
18
  fileNames: ["gemini.md", "gemini-instructions.md"],
18
19
  agents: ["gemini-cli", "antigravity"],
19
20
  },
20
- { fileNames: ["copilot-instructions.md"], agents: ["github-copilot"] },
21
+ { fileNames: ["copilot-instructions.md"], agents: ["claude-code"] },
21
22
  ];
22
23
  // Conventional subdirectory names to scan one level deep for .md files.
23
24
  const AGENT_RULES_SUBDIRS = [
@@ -142,6 +143,9 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
142
143
  // Intersect with active agents; fall back to full active list if empty
143
144
  const intersection = defaultAgents.filter((a) => activeAgents.includes(a));
144
145
  const agents = intersection.length > 0 ? intersection : activeAgents;
146
+ // Skip if no capable agents remain (e.g. --agents github-copilot only)
147
+ if (agents.length === 0)
148
+ continue;
145
149
  // Resolve name collision with skill names
146
150
  let name = rawName;
147
151
  if (namesSeen.has(name)) {
@@ -160,6 +164,121 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
160
164
  }
161
165
  return rules;
162
166
  }
167
+ async function loadMcpServers(baseDir) {
168
+ const filePath = path.join(baseDir, "mcp-servers.json");
169
+ let raw;
170
+ try {
171
+ raw = await readFile(filePath, "utf-8");
172
+ }
173
+ catch {
174
+ return [];
175
+ }
176
+ let parsed;
177
+ try {
178
+ parsed = JSON.parse(raw);
179
+ }
180
+ catch {
181
+ logger.warn("init", "mcp-servers.json: invalid JSON, skipping");
182
+ return [];
183
+ }
184
+ if (!Array.isArray(parsed)) {
185
+ logger.warn("init", "mcp-servers.json: expected a JSON array, skipping");
186
+ return [];
187
+ }
188
+ const results = [];
189
+ for (let i = 0; i < parsed.length; i++) {
190
+ const result = McpServerEntrySchema.safeParse(parsed[i]);
191
+ if (result.success) {
192
+ results.push(result.data);
193
+ }
194
+ else {
195
+ logger.warn("init", `mcp-servers.json: entry[${i}] invalid, skipping`);
196
+ }
197
+ }
198
+ return results;
199
+ }
200
+ async function loadFilesManifest(baseDir) {
201
+ const filePath = path.join(baseDir, "files-manifest.json");
202
+ let raw;
203
+ try {
204
+ raw = await readFile(filePath, "utf-8");
205
+ }
206
+ catch {
207
+ return [];
208
+ }
209
+ let parsed;
210
+ try {
211
+ parsed = JSON.parse(raw);
212
+ }
213
+ catch {
214
+ logger.warn("init", "files-manifest.json: invalid JSON, skipping");
215
+ return [];
216
+ }
217
+ if (!Array.isArray(parsed)) {
218
+ logger.warn("init", "files-manifest.json: expected a JSON array, skipping");
219
+ return [];
220
+ }
221
+ const results = [];
222
+ for (let i = 0; i < parsed.length; i++) {
223
+ const result = FileEntrySchema.safeParse(parsed[i]);
224
+ if (result.success) {
225
+ results.push(result.data);
226
+ }
227
+ else {
228
+ logger.warn("init", `files-manifest.json: entry[${i}] invalid, skipping`);
229
+ }
230
+ }
231
+ return results;
232
+ }
233
+ async function loadConfigsManifest(baseDir) {
234
+ const filePath = path.join(baseDir, "configs-manifest.json");
235
+ let raw;
236
+ try {
237
+ raw = await readFile(filePath, "utf-8");
238
+ }
239
+ catch {
240
+ return [];
241
+ }
242
+ let parsed;
243
+ try {
244
+ parsed = JSON.parse(raw);
245
+ }
246
+ catch {
247
+ logger.warn("init", "configs-manifest.json: invalid JSON, skipping");
248
+ return [];
249
+ }
250
+ if (!Array.isArray(parsed)) {
251
+ logger.warn("init", "configs-manifest.json: expected a JSON array, skipping");
252
+ return [];
253
+ }
254
+ const results = [];
255
+ for (let i = 0; i < parsed.length; i++) {
256
+ const result = ConfigEntrySchema.safeParse(parsed[i]);
257
+ if (result.success) {
258
+ results.push(result.data);
259
+ }
260
+ else {
261
+ logger.warn("init", `configs-manifest.json: entry[${i}] invalid, skipping`);
262
+ }
263
+ }
264
+ return results;
265
+ }
266
+ async function emitDirectoryHints(baseDir, filesLoaded, configsLoaded) {
267
+ for (const [dir, section, loaded, sidecar] of [
268
+ ["files", "files", filesLoaded, "files-manifest.json"],
269
+ ["configs", "configs", configsLoaded, "configs-manifest.json"],
270
+ ]) {
271
+ try {
272
+ await access(path.join(baseDir, dir));
273
+ if (loaded === 0) {
274
+ logger.info(`Detected ${dir}/ directory — create ${sidecar} at the repo root to have init populate ${section} entries automatically.`);
275
+ }
276
+ }
277
+ catch {
278
+ // directory does not exist — silent
279
+ }
280
+ }
281
+ }
163
282
  async function manifestExists(manifestPath) {
164
283
  try {
165
284
  await access(manifestPath);
@@ -169,9 +288,39 @@ async function manifestExists(manifestPath) {
169
288
  return false;
170
289
  }
171
290
  }
291
+ function logVerboseManifest(skills, agentRules, mcpServers, files, configs) {
292
+ for (const s of skills) {
293
+ logger.detail(`${s.name} → ${s.path}`);
294
+ }
295
+ if (agentRules.length > 0) {
296
+ logger.detail("agentRules:");
297
+ for (const r of agentRules) {
298
+ logger.detail(` ${r.name} → ${r.path} [${r.agents.join(", ")}]`);
299
+ }
300
+ }
301
+ if (mcpServers.length > 0) {
302
+ logger.detail("mcpServers:");
303
+ for (const m of mcpServers) {
304
+ logger.detail(` ${m.name} [${m.agents.join(", ")}]`);
305
+ }
306
+ }
307
+ if (files.length > 0) {
308
+ logger.detail("files:");
309
+ for (const f of files) {
310
+ logger.detail(` ${f.name} → ${f.target} [${f.agents.join(", ")}]`);
311
+ }
312
+ }
313
+ if (configs.length > 0) {
314
+ logger.detail("configs:");
315
+ for (const c of configs) {
316
+ logger.detail(` ${c.name} → ${c.target} [${c.agents.join(", ")}]`);
317
+ }
318
+ }
319
+ }
172
320
  export async function runInit(options) {
173
321
  const { directory, dryRun, force, verbose } = options;
174
322
  const agents = options.agents ?? [...AGENT_IDS];
323
+ const agentRulesCapableAgents = agents.filter((id) => AGENT_REGISTRY_BY_ID[id].agentRulesSupport?.status !== "unsupported");
175
324
  const manifestPath = path.join(directory, "inception.json");
176
325
  if (!dryRun && (await manifestExists(manifestPath)) && !force) {
177
326
  logger.error(`Error: ${manifestPath} already exists. Use --force to overwrite.`);
@@ -186,33 +335,40 @@ export async function runInit(options) {
186
335
  const skillNamesSeen = new Set(skills.map((s) => s.name));
187
336
  const skillDirRelPaths = new Set(found.map((f) => f.relPath));
188
337
  const agentRulesCandidates = await findAgentRulesCandidates(directory, skillDirRelPaths);
189
- const agentRules = buildAgentRules(agentRulesCandidates, agents, skillNamesSeen);
338
+ const agentRules = buildAgentRules(agentRulesCandidates, agentRulesCapableAgents, skillNamesSeen);
339
+ const mcpServers = await loadMcpServers(directory);
340
+ const files = await loadFilesManifest(directory);
341
+ const configs = await loadConfigsManifest(directory);
190
342
  const manifest = {
191
343
  skills,
192
- files: [],
193
- configs: [],
194
- mcpServers: [],
344
+ files,
345
+ configs,
346
+ mcpServers,
195
347
  agentRules,
196
348
  };
197
349
  const json = `${JSON.stringify(manifest, null, 2)}\n`;
350
+ function summarize() {
351
+ const parts = [
352
+ `${skills.length} skill(s)`,
353
+ `${agentRules.length} agentRule(s)`,
354
+ `${mcpServers.length} mcpServer(s)`,
355
+ `${files.length} file(s)`,
356
+ `${configs.length} config(s)`,
357
+ ];
358
+ return parts.join(", ");
359
+ }
198
360
  if (dryRun) {
199
- logger.info(`${dryRunPrefix(true)}Would write ${manifestPath} with ${skills.length} skill(s) and ${agentRules.length} agentRule(s):`);
361
+ logger.info(`${dryRunPrefix(true)}Would write ${manifestPath} with ${summarize()}:`);
200
362
  logger.info("");
201
363
  logger.info(json);
364
+ await emitDirectoryHints(directory, files.length, configs.length);
202
365
  return 0;
203
366
  }
204
367
  await writeFile(manifestPath, json, "utf-8");
205
- logger.info(`Generated ${manifestPath} with ${skills.length} skill(s) and ${agentRules.length} agentRule(s).`);
368
+ logger.info(`Generated ${manifestPath} with ${summarize()}.`);
206
369
  if (verbose) {
207
- for (const s of skills) {
208
- logger.detail(`${s.name} → ${s.path}`);
209
- }
210
- if (agentRules.length > 0) {
211
- logger.detail("agentRules:");
212
- for (const r of agentRules) {
213
- logger.detail(` ${r.name} → ${r.path} [${r.agents.join(", ")}]`);
214
- }
215
- }
370
+ logVerboseManifest(skills, agentRules, mcpServers, files, configs);
216
371
  }
372
+ await emitDirectoryHints(directory, files.length, configs.length);
217
373
  return 0;
218
374
  }
@@ -1,7 +1,7 @@
1
1
  import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
2
2
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
3
3
  import { logger } from "../logger.js";
4
- import { compileAgentRuleReverts, compileMcpServerReverts, } from "./adapters/index.js";
4
+ import { compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
5
5
  import { revertTomlMcpPatch } from "./adapters/toml.js";
6
6
  import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
7
7
  import { resolveAgentSkillPath } from "./resolve.js";
@@ -70,6 +70,7 @@ export function planRevert(manifest, detectedAgents, home, repo) {
70
70
  ...buildConfigPatchReverts(manifest, home, detectedAgents),
71
71
  ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home, repo)),
72
72
  ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home, repo)),
73
+ ...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, detectedAgents, home)),
73
74
  ];
74
75
  }
75
76
  export function planRevertAll(manifest, home, repo) {
@@ -79,6 +80,7 @@ export function planRevertAll(manifest, home, repo) {
79
80
  ...buildConfigPatchReverts(manifest, home, null),
80
81
  ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home, repo)),
81
82
  ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home, repo)),
83
+ ...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, null, home)),
82
84
  ];
83
85
  }
84
86
  function recordOutcome(result, action, counts, failed) {
@@ -2,5 +2,6 @@ export declare function sourceAccessError(err: unknown, sourcePath: string): str
2
2
  export declare function validateSourcePath(source: string, skillPath: string, resolvedSourceDir: string, realRoot: string): Promise<void>;
3
3
  export declare function validateSourceFile(sourcePath: string, manifestPath: string): Promise<void>;
4
4
  export declare function validateMcpServerConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
5
+ export declare function validatePermissionsConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
5
6
  export declare function validateAgentRuleMarkdownPath(manifestPath: string, agentId: string): void;
6
7
  export declare function validateSkillDefinitionFile(sourcePath: string, manifestPath: string): Promise<void>;
@@ -96,6 +96,51 @@ export function validateMcpServerConfigShape(config, entryName, agentId) {
96
96
  throw new UserError("DEPLOY_FAILED", `mcpServers entry "${entryName}" for agent "${agentId}" must define "env" as an object of string values when present`);
97
97
  }
98
98
  }
99
+ const CODEX_APPROVAL_POLICY_VALUES = [
100
+ "auto",
101
+ "manual",
102
+ "suggest",
103
+ "on-failure",
104
+ ];
105
+ function rejectUnknownKeys(config, allowed, entryName, agentId) {
106
+ const unknownKeys = Object.keys(config).filter((k) => !allowed.includes(k));
107
+ if (unknownKeys.length > 0) {
108
+ throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "${agentId}" contains unrecognized keys: ${unknownKeys.join(", ")}. Only ${allowed.map((k) => `"${k}"`).join(", ")} is allowed.`);
109
+ }
110
+ }
111
+ function validateClaudeCodePermissions(config, entryName) {
112
+ rejectUnknownKeys(config, ["permissions"], entryName, "claude-code");
113
+ const perms = config.permissions;
114
+ if (perms === undefined)
115
+ return;
116
+ if (typeof perms !== "object" || perms === null || Array.isArray(perms)) {
117
+ throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "claude-code" must define "permissions" as an object`);
118
+ }
119
+ const permsObj = perms;
120
+ for (const key of ["allow", "deny"]) {
121
+ const val = permsObj[key];
122
+ if (val !== undefined &&
123
+ (!Array.isArray(val) || val.some((item) => typeof item !== "string"))) {
124
+ throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "claude-code" must define "permissions.${key}" as an array of strings when present`);
125
+ }
126
+ }
127
+ }
128
+ function validateCodexPermissions(config, entryName) {
129
+ rejectUnknownKeys(config, ["approval_policy"], entryName, "codex");
130
+ const policy = config.approval_policy;
131
+ if (policy !== undefined &&
132
+ !CODEX_APPROVAL_POLICY_VALUES.includes(policy)) {
133
+ throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "codex" must define "approval_policy" as one of: ${CODEX_APPROVAL_POLICY_VALUES.join(", ")}`);
134
+ }
135
+ }
136
+ export function validatePermissionsConfigShape(config, entryName, agentId) {
137
+ if (agentId === "claude-code") {
138
+ validateClaudeCodePermissions(config, entryName);
139
+ }
140
+ else if (agentId === "codex") {
141
+ validateCodexPermissions(config, entryName);
142
+ }
143
+ }
99
144
  export function validateAgentRuleMarkdownPath(manifestPath, agentId) {
100
145
  const extension = path.extname(manifestPath).toLowerCase();
101
146
  if (extension !== ".md" && extension !== ".markdown") {
@@ -72,6 +72,18 @@ export declare const AgentRuleEntrySchema: z.ZodObject<{
72
72
  }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
73
73
  path: z.ZodString;
74
74
  }, z.core.$strip>;
75
+ export declare const PermissionsEntrySchema: z.ZodObject<{
76
+ name: z.ZodString;
77
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
78
+ "claude-code": "claude-code";
79
+ codex: "codex";
80
+ "gemini-cli": "gemini-cli";
81
+ antigravity: "antigravity";
82
+ opencode: "opencode";
83
+ "github-copilot": "github-copilot";
84
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
85
+ config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
86
+ }, z.core.$strip>;
75
87
  export declare const ManifestSchema: z.ZodObject<{
76
88
  skills: z.ZodArray<z.ZodObject<{
77
89
  name: z.ZodString;
@@ -135,12 +147,25 @@ export declare const ManifestSchema: z.ZodObject<{
135
147
  }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
136
148
  path: z.ZodString;
137
149
  }, z.core.$strip>>>;
150
+ permissions: z.ZodDefault<z.ZodArray<z.ZodObject<{
151
+ name: z.ZodString;
152
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
153
+ "claude-code": "claude-code";
154
+ codex: "codex";
155
+ "gemini-cli": "gemini-cli";
156
+ antigravity: "antigravity";
157
+ opencode: "opencode";
158
+ "github-copilot": "github-copilot";
159
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
160
+ config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
161
+ }, z.core.$strip>>>;
138
162
  }, z.core.$strip>;
139
163
  export type SkillEntry = z.infer<typeof SkillEntrySchema>;
140
164
  export type FileEntry = z.infer<typeof FileEntrySchema>;
141
165
  export type ConfigEntry = z.infer<typeof ConfigEntrySchema>;
142
166
  export type McpServerEntry = z.infer<typeof McpServerEntrySchema>;
143
167
  export type AgentRuleEntry = z.infer<typeof AgentRuleEntrySchema>;
168
+ export type PermissionsEntry = z.infer<typeof PermissionsEntrySchema>;
144
169
  export type Manifest = z.infer<typeof ManifestSchema>;
145
170
  export declare const AgentListSchema: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
146
171
  "claude-code": "claude-code";
@@ -90,6 +90,14 @@ export const AgentRuleEntrySchema = z.object({
90
90
  // Relative path to the rules/instruction file within the source bundle.
91
91
  path: sourcePathField,
92
92
  });
93
+ export const PermissionsEntrySchema = z.object({
94
+ name: nameField,
95
+ agents: agentsField,
96
+ // Raw permission config payload validated per agent by the permissions adapter.
97
+ // For claude-code: { permissions: { allow?: string[], deny?: string[] } }
98
+ // For codex: { approval_policy?: "auto" | "manual" | "suggest" | "on-failure" }
99
+ config: z.record(z.string(), z.unknown()),
100
+ });
93
101
  export const ManifestSchema = z.object({
94
102
  skills: z.array(SkillEntrySchema).superRefine((skills, ctx) => {
95
103
  const seen = new Set();
@@ -108,6 +116,7 @@ export const ManifestSchema = z.object({
108
116
  configs: z.array(ConfigEntrySchema).default([]),
109
117
  mcpServers: z.array(McpServerEntrySchema).default([]),
110
118
  agentRules: z.array(AgentRuleEntrySchema).default([]),
119
+ permissions: z.array(PermissionsEntrySchema).default([]),
111
120
  });
112
121
  // Parses the --agents CLI flag: comma-separated agent IDs → AgentId[]
113
122
  export const AgentListSchema = z
package/dist/types.d.ts CHANGED
@@ -23,6 +23,7 @@ export interface AgentProvenance {
23
23
  detectBinary: Confidence;
24
24
  mcpConfig?: Confidence;
25
25
  agentRules?: Confidence;
26
+ permissions?: Confidence;
26
27
  }
27
28
  export interface AgentConfig {
28
29
  id: AgentId;
@@ -33,6 +34,7 @@ export interface AgentConfig {
33
34
  provenance: AgentProvenance;
34
35
  mcpSupport?: AgentSurfaceSupport;
35
36
  agentRulesSupport?: AgentSurfaceSupport;
37
+ permissionsSupport?: AgentSurfaceSupport;
36
38
  policyNote?: string;
37
39
  }
38
40
  export interface PlanWarning {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "Deploy AI agent skills from a git repo to user home directories",
5
5
  "license": "MIT",
6
6
  "author": "Damian Piątkowski",