@sayknow-cli/coding-agent 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/notify-cli.d.ts +2 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/config/settings-schema.d.ts +39 -2
  12. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  13. package/dist/types/defaults/skc-defaults.d.ts +2 -13
  14. package/dist/types/extensibility/shared-events.d.ts +1 -0
  15. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  16. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  17. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  18. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  20. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  21. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  22. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  23. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  24. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  25. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  26. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  27. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  28. package/dist/types/lsp/types.d.ts +2 -0
  29. package/dist/types/main.d.ts +2 -0
  30. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  31. package/dist/types/modes/components/model-selector.d.ts +8 -0
  32. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  33. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  34. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  35. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  36. package/dist/types/notifications/config.d.ts +9 -1
  37. package/dist/types/notifications/engine.d.ts +59 -0
  38. package/dist/types/notifications/html-format.d.ts +11 -0
  39. package/dist/types/notifications/index.d.ts +149 -1
  40. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  41. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  42. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  43. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  44. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  45. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  46. package/dist/types/notifications/recent-activity.d.ts +35 -0
  47. package/dist/types/notifications/telegram-daemon.d.ts +133 -16
  48. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  49. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  50. package/dist/types/notifications/threaded-render.d.ts +6 -1
  51. package/dist/types/notifications/topic-registry.d.ts +12 -9
  52. package/dist/types/runtime-mcp/types.d.ts +7 -0
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +16 -4
  55. package/dist/types/session/blob-store.d.ts +25 -0
  56. package/dist/types/session/session-manager.d.ts +57 -0
  57. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  58. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  59. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  60. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  61. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  62. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  63. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  64. package/dist/types/system-prompt.d.ts +2 -0
  65. package/dist/types/task/executor.d.ts +9 -1
  66. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  67. package/dist/types/tools/fetch.d.ts +23 -0
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/telegram-send.d.ts +32 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/bridge.d.ts +103 -0
  72. package/dist/types/web/insane/url-guard.d.ts +25 -0
  73. package/dist/types/web/scrapers/types.d.ts +5 -0
  74. package/dist/types/web/scrapers/utils.d.ts +7 -1
  75. package/dist/types/web/search/provider.d.ts +18 -1
  76. package/dist/types/web/search/providers/insane.d.ts +53 -0
  77. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  78. package/dist/types/web/search/types.d.ts +12 -4
  79. package/package.json +14 -10
  80. package/scripts/g004-tmux-smoke.ts +100 -0
  81. package/scripts/g005-daemon-smoke.ts +180 -0
  82. package/scripts/g011-daemon-path-smoke.ts +153 -0
  83. package/scripts/verify-insane-vendor.ts +132 -0
  84. package/src/cli/args.ts +1 -1
  85. package/src/cli/fast-help.ts +30 -2
  86. package/src/cli/mcp-cli.ts +272 -0
  87. package/src/cli/notify-cli.ts +152 -5
  88. package/src/cli/plugin-cli.ts +66 -3
  89. package/src/cli.ts +30 -12
  90. package/src/commands/mcp.ts +117 -0
  91. package/src/commands/plugin.ts +4 -0
  92. package/src/commands/session.ts +18 -0
  93. package/src/commands/team.ts +1 -1
  94. package/src/config/keybindings.ts +2 -2
  95. package/src/config/model-profile-activation.ts +55 -7
  96. package/src/config/settings-schema.ts +30 -1
  97. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  98. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  99. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  100. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  101. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  103. package/src/defaults/skc-defaults.ts +2 -27
  104. package/src/export/html/index.ts +2 -2
  105. package/src/extensibility/extensions/runner.ts +1 -0
  106. package/src/extensibility/shared-events.ts +1 -0
  107. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  108. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  109. package/src/extensibility/skc-plugins/index.ts +9 -0
  110. package/src/extensibility/skc-plugins/injection.ts +109 -0
  111. package/src/extensibility/skc-plugins/installer.ts +434 -0
  112. package/src/extensibility/skc-plugins/loader.ts +3 -1
  113. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  114. package/src/extensibility/skc-plugins/observability.ts +84 -0
  115. package/src/extensibility/skc-plugins/paths.ts +1 -1
  116. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  117. package/src/extensibility/skc-plugins/registry.ts +180 -0
  118. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  119. package/src/extensibility/skc-plugins/schema.ts +250 -20
  120. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  121. package/src/extensibility/skc-plugins/types.ts +199 -3
  122. package/src/extensibility/skc-plugins/validation.ts +80 -0
  123. package/src/extensibility/skills.ts +15 -0
  124. package/src/hooks/skill-state.ts +57 -0
  125. package/src/internal-urls/docs-index.generated.ts +17 -13
  126. package/src/lsp/config.ts +16 -3
  127. package/src/lsp/defaults.json +7 -0
  128. package/src/lsp/types.ts +2 -0
  129. package/src/main.ts +14 -3
  130. package/src/modes/bridge/bridge-mode.ts +11 -0
  131. package/src/modes/components/assistant-message.ts +49 -1
  132. package/src/modes/components/custom-editor.ts +2 -0
  133. package/src/modes/components/footer.ts +2 -3
  134. package/src/modes/components/hook-editor.ts +1 -1
  135. package/src/modes/components/hook-selector.ts +67 -43
  136. package/src/modes/components/model-selector.ts +56 -11
  137. package/src/modes/components/status-line/git-utils.ts +25 -0
  138. package/src/modes/components/status-line.ts +10 -11
  139. package/src/modes/components/welcome.ts +2 -3
  140. package/src/modes/controllers/event-controller.ts +15 -0
  141. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  142. package/src/modes/controllers/selector-controller.ts +53 -11
  143. package/src/modes/interactive-mode.ts +50 -3
  144. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  145. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  146. package/src/modes/theme/defaults/index.ts +2 -0
  147. package/src/modes/utils/context-usage.ts +2 -2
  148. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  149. package/src/notifications/attachment-registry.ts +23 -0
  150. package/src/notifications/chat-adapters.ts +147 -0
  151. package/src/notifications/config.ts +23 -2
  152. package/src/notifications/engine.ts +100 -0
  153. package/src/notifications/html-format.ts +38 -0
  154. package/src/notifications/index.ts +417 -45
  155. package/src/notifications/lifecycle-commands.ts +238 -0
  156. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  157. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  158. package/src/notifications/managed-daemon.ts +163 -0
  159. package/src/notifications/operator-runtime.ts +171 -0
  160. package/src/notifications/rate-limit-pool.ts +19 -0
  161. package/src/notifications/recent-activity.ts +132 -0
  162. package/src/notifications/telegram-daemon.ts +984 -242
  163. package/src/notifications/telegram-reference.ts +25 -7
  164. package/src/notifications/threaded-inbound.ts +60 -4
  165. package/src/notifications/threaded-render.ts +20 -2
  166. package/src/notifications/topic-registry.ts +23 -9
  167. package/src/prompts/agents/executor.md +2 -2
  168. package/src/runtime-mcp/transports/stdio.ts +38 -4
  169. package/src/runtime-mcp/types.ts +7 -0
  170. package/src/sdk.ts +157 -10
  171. package/src/session/agent-session.ts +248 -125
  172. package/src/session/blob-store.ts +196 -8
  173. package/src/session/session-manager.ts +762 -12
  174. package/src/skc-runtime/launch-tmux.ts +85 -22
  175. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  176. package/src/skc-runtime/psmux-detect.ts +239 -0
  177. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  178. package/src/skc-runtime/team-runtime.ts +56 -23
  179. package/src/skc-runtime/tmux-common.ts +88 -4
  180. package/src/skc-runtime/tmux-sessions.ts +111 -9
  181. package/src/skc-runtime/ultragoal-guard.ts +25 -8
  182. package/src/skc-runtime/ultragoal-runtime.ts +75 -15
  183. package/src/skc-runtime/workflow-manifest.generated.json +29 -0
  184. package/src/skc-runtime/workflow-manifest.ts +7 -2
  185. package/src/slash-commands/builtin-registry.ts +23 -3
  186. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  187. package/src/slash-commands/helpers/parse.ts +2 -1
  188. package/src/system-prompt.ts +9 -0
  189. package/src/task/executor.ts +31 -7
  190. package/src/task/index.ts +2 -0
  191. package/src/tools/ask.ts +5 -1
  192. package/src/tools/bash.ts +9 -0
  193. package/src/tools/composer-bash-policy.ts +96 -0
  194. package/src/tools/fetch.ts +94 -1
  195. package/src/tools/index.ts +6 -1
  196. package/src/tools/telegram-send.ts +137 -0
  197. package/src/utils/changelog.ts +8 -0
  198. package/src/web/insane/bridge.ts +350 -0
  199. package/src/web/insane/url-guard.ts +159 -0
  200. package/src/web/scrapers/types.ts +143 -45
  201. package/src/web/scrapers/utils.ts +70 -19
  202. package/src/web/search/provider.ts +77 -18
  203. package/src/web/search/providers/anthropic.ts +70 -3
  204. package/src/web/search/providers/codex.ts +1 -119
  205. package/src/web/search/providers/gemini.ts +99 -0
  206. package/src/web/search/providers/insane.ts +551 -0
  207. package/src/web/search/providers/openai-compatible.ts +66 -32
  208. package/src/web/search/providers/text-citations.ts +111 -0
  209. package/src/web/search/types.ts +13 -2
  210. package/vendor/insane-search/LICENSE +21 -0
  211. package/vendor/insane-search/MANIFEST.json +24 -0
  212. package/vendor/insane-search/engine/__init__.py +23 -0
  213. package/vendor/insane-search/engine/__main__.py +128 -0
  214. package/vendor/insane-search/engine/bias_check.py +183 -0
  215. package/vendor/insane-search/engine/executor.py +254 -0
  216. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  217. package/vendor/insane-search/engine/learning.py +175 -0
  218. package/vendor/insane-search/engine/phase0.py +214 -0
  219. package/vendor/insane-search/engine/safety.py +91 -0
  220. package/vendor/insane-search/engine/templates/package.json +11 -0
  221. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  222. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  223. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  224. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  225. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  226. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  227. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  228. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  229. package/vendor/insane-search/engine/transport.py +211 -0
  230. package/vendor/insane-search/engine/url_transforms.py +98 -0
  231. package/vendor/insane-search/engine/validators.py +331 -0
  232. package/vendor/insane-search/engine/waf_detector.py +214 -0
  233. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -1,6 +1,43 @@
1
- import { SKC_PLUGIN_KIND, SkcPluginLoadError, type SkcPluginManifest, type SubskillFrontmatter } from "./types";
1
+ import {
2
+ SKC_PLUGIN_KIND,
3
+ SKC_SUBSKILL_PARENT_AGENTS,
4
+ type SkcPluginAgentAppendixManifestEntry,
5
+ type SkcPluginAppendixManifestEntry,
6
+ type SkcPluginHookManifestEntry,
7
+ SkcPluginLoadError,
8
+ type SkcPluginManifest,
9
+ type SkcPluginMcpManifestEntry,
10
+ type SkcPluginMcpTransport,
11
+ type SkcPluginToolManifestEntry,
12
+ type SkcSubskillParentAgent,
13
+ type SubskillFrontmatter,
14
+ } from "./types";
2
15
 
3
- const FORBIDDEN_MANIFEST_KEYS = ["skills", "slash-commands", "commands", "hooks", "mcp", "mcpServers", "agents"];
16
+ /**
17
+ * Top-level surfaces that may never appear in a SKC plugin bundle: bundles may
18
+ * only EXTEND existing skills/agents, never register new top-level ones.
19
+ */
20
+ const FORBIDDEN_MANIFEST_KEYS = ["skills", "slash-commands", "commands", "agents"];
21
+
22
+ /**
23
+ * Ambiguous legacy aliases. `mcps` is the only canonical MCP key; these are
24
+ * rejected as `unsupported_surface` to avoid accidental legacy shape ambiguity.
25
+ */
26
+ const UNSUPPORTED_ALIAS_KEYS = ["mcp", "mcpServers"];
27
+
28
+ const KNOWN_MANIFEST_KEYS = new Set([
29
+ "kind",
30
+ "name",
31
+ "version",
32
+ "subskills",
33
+ "tools",
34
+ "hooks",
35
+ "mcps",
36
+ "system_appendix",
37
+ "agent-appendix",
38
+ ]);
39
+
40
+ const MCP_TRANSPORTS: readonly SkcPluginMcpTransport[] = ["stdio", "http", "sse"];
4
41
 
5
42
  function isRecord(value: unknown): value is Record<string, unknown> {
6
43
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -16,14 +53,194 @@ function requireNonEmptyString(value: unknown, field: string, filePath: string):
16
53
  return value;
17
54
  }
18
55
 
19
- function requireStringArray(value: unknown, field: string, manifestPath: string): string[] {
56
+ function manifestString(value: unknown, field: string, manifestPath: string): string {
57
+ if (typeof value !== "string" || value.trim().length === 0) {
58
+ throw new SkcPluginLoadError(
59
+ "invalid_manifest",
60
+ `Invalid SKC plugin manifest at ${manifestPath}: ${field} must be a non-empty string`,
61
+ );
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function optionalStringArray(value: unknown, field: string, manifestPath: string): string[] {
67
+ if (value === undefined) return [];
20
68
  if (!Array.isArray(value) || !value.every(item => typeof item === "string")) {
21
69
  throw new SkcPluginLoadError(
22
70
  "invalid_manifest",
23
71
  `Invalid SKC plugin manifest at ${manifestPath}: ${field} must be a string array`,
24
72
  );
25
73
  }
26
- return [...value];
74
+ return [...(value as string[])];
75
+ }
76
+
77
+ function optionalArray(value: unknown, field: string, manifestPath: string): unknown[] {
78
+ if (value === undefined) return [];
79
+ if (!Array.isArray(value)) {
80
+ throw new SkcPluginLoadError(
81
+ "invalid_manifest",
82
+ `Invalid SKC plugin manifest at ${manifestPath}: ${field} must be an array`,
83
+ );
84
+ }
85
+ return value;
86
+ }
87
+
88
+ function deriveToolName(toolPath: string): string {
89
+ const base = toolPath.split("/").pop() ?? toolPath;
90
+ return base.replace(/\.[^.]+$/, "");
91
+ }
92
+
93
+ function parseTools(value: unknown, manifestPath: string): SkcPluginToolManifestEntry[] {
94
+ const raw = optionalArray(value, "tools", manifestPath);
95
+ return raw.map((entry, index) => {
96
+ // Legacy string shorthand: subskill-scoped tool path only.
97
+ if (typeof entry === "string") {
98
+ if (entry.trim().length === 0) {
99
+ throw new SkcPluginLoadError(
100
+ "invalid_manifest",
101
+ `Invalid SKC plugin manifest at ${manifestPath}: tools[${index}] must be a non-empty path`,
102
+ );
103
+ }
104
+ return { name: deriveToolName(entry), path: entry, surface: "subskill" };
105
+ }
106
+ if (!isRecord(entry)) {
107
+ throw new SkcPluginLoadError(
108
+ "invalid_manifest",
109
+ `Invalid SKC plugin manifest at ${manifestPath}: tools[${index}] must be a string or object`,
110
+ );
111
+ }
112
+ const name = manifestString(entry.name, `tools[${index}].name`, manifestPath);
113
+ const path = manifestString(entry.path, `tools[${index}].path`, manifestPath);
114
+ const description =
115
+ entry.description === undefined
116
+ ? undefined
117
+ : manifestString(entry.description, `tools[${index}].description`, manifestPath);
118
+ const sha256 =
119
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `tools[${index}].sha256`, manifestPath);
120
+ return { name, path, description, sha256, surface: "always-on" };
121
+ });
122
+ }
123
+
124
+ function parseHooks(value: unknown, manifestPath: string): SkcPluginHookManifestEntry[] {
125
+ const raw = optionalArray(value, "hooks", manifestPath);
126
+ return raw.map((entry, index) => {
127
+ if (!isRecord(entry)) {
128
+ throw new SkcPluginLoadError(
129
+ "invalid_manifest",
130
+ `Invalid SKC plugin manifest at ${manifestPath}: hooks[${index}] must be an object`,
131
+ );
132
+ }
133
+ const name = manifestString(entry.name, `hooks[${index}].name`, manifestPath);
134
+ const event = manifestString(entry.event, `hooks[${index}].event`, manifestPath);
135
+ const path = manifestString(entry.path, `hooks[${index}].path`, manifestPath);
136
+ const target =
137
+ entry.target === undefined ? undefined : manifestString(entry.target, `hooks[${index}].target`, manifestPath);
138
+ let phase: "before" | "after" | undefined;
139
+ if (entry.phase !== undefined) {
140
+ if (entry.phase !== "before" && entry.phase !== "after") {
141
+ throw new SkcPluginLoadError(
142
+ "invalid_manifest",
143
+ `Invalid SKC plugin manifest at ${manifestPath}: hooks[${index}].phase must be "before" or "after"`,
144
+ );
145
+ }
146
+ phase = entry.phase;
147
+ }
148
+ const sha256 =
149
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `hooks[${index}].sha256`, manifestPath);
150
+ return { name, event, target, phase, path, sha256 };
151
+ });
152
+ }
153
+
154
+ function parseMcps(value: unknown, manifestPath: string): SkcPluginMcpManifestEntry[] {
155
+ const raw = optionalArray(value, "mcps", manifestPath);
156
+ return raw.map((entry, index) => {
157
+ if (!isRecord(entry)) {
158
+ throw new SkcPluginLoadError(
159
+ "invalid_manifest",
160
+ `Invalid SKC plugin manifest at ${manifestPath}: mcps[${index}] must be an object`,
161
+ );
162
+ }
163
+ const name = manifestString(entry.name, `mcps[${index}].name`, manifestPath);
164
+ const transport = entry.transport;
165
+ if (typeof transport !== "string" || !MCP_TRANSPORTS.includes(transport as SkcPluginMcpTransport)) {
166
+ throw new SkcPluginLoadError(
167
+ "invalid_manifest",
168
+ `Invalid SKC plugin manifest at ${manifestPath}: mcps[${index}].transport must be one of ${MCP_TRANSPORTS.join(", ")}`,
169
+ );
170
+ }
171
+ const command =
172
+ entry.command === undefined
173
+ ? undefined
174
+ : manifestString(entry.command, `mcps[${index}].command`, manifestPath);
175
+ const url = entry.url === undefined ? undefined : manifestString(entry.url, `mcps[${index}].url`, manifestPath);
176
+ const cwd = entry.cwd === undefined ? undefined : manifestString(entry.cwd, `mcps[${index}].cwd`, manifestPath);
177
+ let args: string[] | undefined;
178
+ if (entry.args !== undefined) {
179
+ if (!Array.isArray(entry.args) || !entry.args.every(item => typeof item === "string")) {
180
+ throw new SkcPluginLoadError(
181
+ "invalid_manifest",
182
+ `Invalid SKC plugin manifest at ${manifestPath}: mcps[${index}].args must be a string array`,
183
+ );
184
+ }
185
+ args = [...(entry.args as string[])];
186
+ }
187
+ let headers: Record<string, string> | undefined;
188
+ if (entry.headers !== undefined) {
189
+ if (!isRecord(entry.headers) || !Object.values(entry.headers).every(v => typeof v === "string")) {
190
+ throw new SkcPluginLoadError(
191
+ "invalid_manifest",
192
+ `Invalid SKC plugin manifest at ${manifestPath}: mcps[${index}].headers must be a string map`,
193
+ );
194
+ }
195
+ headers = { ...(entry.headers as Record<string, string>) };
196
+ }
197
+ const sha256 =
198
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `mcps[${index}].sha256`, manifestPath);
199
+ return { name, transport: transport as SkcPluginMcpTransport, command, args, cwd, url, headers, sha256 };
200
+ });
201
+ }
202
+
203
+ function parseAppendixEntry(entry: unknown, field: string, manifestPath: string): SkcPluginAppendixManifestEntry {
204
+ if (!isRecord(entry)) {
205
+ throw new SkcPluginLoadError(
206
+ "invalid_manifest",
207
+ `Invalid SKC plugin manifest at ${manifestPath}: ${field} must be an object`,
208
+ );
209
+ }
210
+ const name = manifestString(entry.name, `${field}.name`, manifestPath);
211
+ const path = entry.path === undefined ? undefined : manifestString(entry.path, `${field}.path`, manifestPath);
212
+ // Content may be empty/whitespace here; the compiler enforces non-empty and
213
+ // maps emptiness to invalid_appendix (not invalid_manifest).
214
+ if (entry.content !== undefined && typeof entry.content !== "string") {
215
+ throw new SkcPluginLoadError(
216
+ "invalid_manifest",
217
+ `Invalid SKC plugin manifest at ${manifestPath}: ${field}.content must be a string`,
218
+ );
219
+ }
220
+ const content = entry.content as string | undefined;
221
+ const sha256 =
222
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `${field}.sha256`, manifestPath);
223
+ return { name, path, content, sha256 };
224
+ }
225
+
226
+ function parseSystemAppendix(value: unknown, manifestPath: string): SkcPluginAppendixManifestEntry[] {
227
+ const raw = optionalArray(value, "system_appendix", manifestPath);
228
+ return raw.map((entry, index) => parseAppendixEntry(entry, `system_appendix[${index}]`, manifestPath));
229
+ }
230
+
231
+ function parseAgentAppendix(value: unknown, manifestPath: string): SkcPluginAgentAppendixManifestEntry[] {
232
+ const raw = optionalArray(value, "agent-appendix", manifestPath);
233
+ return raw.map((entry, index) => {
234
+ const base = parseAppendixEntry(entry, `agent-appendix[${index}]`, manifestPath);
235
+ const agent = (entry as Record<string, unknown>).agent;
236
+ if (typeof agent !== "string" || !SKC_SUBSKILL_PARENT_AGENTS.includes(agent as SkcSubskillParentAgent)) {
237
+ throw new SkcPluginLoadError(
238
+ "invalid_parent",
239
+ `Invalid SKC plugin manifest at ${manifestPath}: agent-appendix[${index}].agent must be one of ${SKC_SUBSKILL_PARENT_AGENTS.join(", ")}`,
240
+ );
241
+ }
242
+ return { ...base, agent: agent as SkcSubskillParentAgent };
243
+ });
27
244
  }
28
245
 
29
246
  export function parseManifest(raw: unknown, manifestPath: string): SkcPluginManifest {
@@ -40,31 +257,44 @@ export function parseManifest(raw: unknown, manifestPath: string): SkcPluginMani
40
257
  }
41
258
  }
42
259
 
260
+ for (const key of UNSUPPORTED_ALIAS_KEYS) {
261
+ if (Object.hasOwn(raw, key)) {
262
+ throw new SkcPluginLoadError(
263
+ "unsupported_surface",
264
+ `Unsupported SKC plugin surface in ${manifestPath}: ${key} (use the canonical "mcps" key)`,
265
+ );
266
+ }
267
+ }
268
+
269
+ for (const key of Object.keys(raw)) {
270
+ if (!KNOWN_MANIFEST_KEYS.has(key)) {
271
+ throw new SkcPluginLoadError(
272
+ "unsupported_surface",
273
+ `Unsupported SKC plugin surface in ${manifestPath}: ${key}`,
274
+ );
275
+ }
276
+ }
277
+
43
278
  if (raw.kind !== SKC_PLUGIN_KIND) {
44
279
  throw new SkcPluginLoadError(
45
280
  "invalid_kind",
46
281
  `Invalid SKC plugin kind in ${manifestPath}: expected ${SKC_PLUGIN_KIND}`,
47
282
  );
48
283
  }
49
- if (typeof raw.name !== "string" || raw.name.trim().length === 0) {
50
- throw new SkcPluginLoadError(
51
- "invalid_manifest",
52
- `Invalid SKC plugin manifest at ${manifestPath}: name must be a non-empty string`,
53
- );
54
- }
55
- if (typeof raw.version !== "string" || raw.version.trim().length === 0) {
56
- throw new SkcPluginLoadError(
57
- "invalid_manifest",
58
- `Invalid SKC plugin manifest at ${manifestPath}: version must be a non-empty string`,
59
- );
60
- }
284
+
285
+ const name = manifestString(raw.name, "name", manifestPath);
286
+ const version = manifestString(raw.version, "version", manifestPath);
61
287
 
62
288
  return {
63
- name: raw.name,
64
- version: raw.version,
289
+ name,
290
+ version,
65
291
  kind: SKC_PLUGIN_KIND,
66
- subskills: requireStringArray(raw.subskills, "subskills", manifestPath),
67
- tools: requireStringArray(raw.tools, "tools", manifestPath),
292
+ subskills: optionalStringArray(raw.subskills, "subskills", manifestPath),
293
+ tools: parseTools(raw.tools, manifestPath),
294
+ hooks: parseHooks(raw.hooks, manifestPath),
295
+ mcps: parseMcps(raw.mcps, manifestPath),
296
+ systemAppendix: parseSystemAppendix(raw.system_appendix, manifestPath),
297
+ agentAppendix: parseAgentAppendix(raw["agent-appendix"], manifestPath),
68
298
  };
69
299
  }
70
300
 
@@ -0,0 +1,147 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import type { SkcPluginLoadErrorCode, SkcPluginRegistryEntry } from "./types";
5
+
6
+ /**
7
+ * Session-start validation: the registry is the collision authority. Capability
8
+ * provider output is supplied as EVIDENCE only; plugin surfaces are never
9
+ * resolved by capability first-wins. Offending surfaces are quarantined
10
+ * fail-closed with a stable error code rather than silently shadowed.
11
+ */
12
+
13
+ export interface SessionCapabilityEvidence {
14
+ /** Built-in + provider tool names already present before plugin insertion. */
15
+ toolNames?: Iterable<string>;
16
+ /** Non-plugin MCP server names from providers/built-ins. */
17
+ mcpNames?: Iterable<string>;
18
+ /** Non-plugin hook keys (event:phase:target:name) from providers/built-ins. */
19
+ hookKeys?: Iterable<string>;
20
+ /** Existing appendix extension ids already present. */
21
+ appendixIds?: Iterable<string>;
22
+ }
23
+
24
+ export interface SessionQuarantine {
25
+ plugin: string;
26
+ surfaceId: string;
27
+ code: SkcPluginLoadErrorCode;
28
+ message: string;
29
+ }
30
+
31
+ export interface SessionValidationResult {
32
+ /** Registry entries (enabled, non-quarantined) whose surfaces may activate. */
33
+ active: SkcPluginRegistryEntry[];
34
+ /** Per-surface quarantine records (fail-closed). */
35
+ quarantine: SessionQuarantine[];
36
+ }
37
+
38
+ function sha256(buf: Buffer): string {
39
+ return createHash("sha256").update(buf).digest("hex");
40
+ }
41
+
42
+ /**
43
+ * Re-verify that the installed files still match the registry's recorded hashes.
44
+ * Drift (manual edits, partial writes) quarantines the whole plugin with
45
+ * runtime_mismatch.
46
+ */
47
+ export async function verifyEntryHashes(entry: SkcPluginRegistryEntry): Promise<SessionQuarantine | null> {
48
+ for (const file of entry.copiedFiles) {
49
+ const abs = path.join(entry.pluginRoot, file.relativePath);
50
+ let buf: Buffer;
51
+ try {
52
+ buf = await fs.readFile(abs);
53
+ } catch {
54
+ return {
55
+ plugin: entry.name,
56
+ surfaceId: `plugin:${entry.name}`,
57
+ code: "runtime_mismatch",
58
+ message: `Installed file missing: ${file.relativePath}`,
59
+ };
60
+ }
61
+ if (sha256(buf) !== file.sha256) {
62
+ return {
63
+ plugin: entry.name,
64
+ surfaceId: `plugin:${entry.name}`,
65
+ code: "runtime_mismatch",
66
+ message: `Installed file hash drift: ${file.relativePath}`,
67
+ };
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function activeSurfaceIds(entry: SkcPluginRegistryEntry): {
74
+ tools: { id: string; name: string }[];
75
+ mcps: { id: string; name: string }[];
76
+ hooks: { id: string }[];
77
+ appendices: { id: string }[];
78
+ } {
79
+ const disabled = new Set(entry.disabledSurfaceIds);
80
+ return {
81
+ tools: entry.surfaces.tools
82
+ .filter(t => !disabled.has(t.extensionId))
83
+ .map(t => ({ id: t.extensionId, name: t.name })),
84
+ mcps: entry.surfaces.mcps
85
+ .filter(m => !disabled.has(m.extensionId))
86
+ .map(m => ({ id: m.extensionId, name: m.name })),
87
+ hooks: entry.surfaces.hooks.filter(h => !disabled.has(h.extensionId)).map(h => ({ id: h.extensionId })),
88
+ appendices: [...entry.surfaces.systemAppendices, ...entry.surfaces.agentAppendices]
89
+ .filter(a => !disabled.has(a.extensionId))
90
+ .map(a => ({ id: a.extensionId })),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Validate the effective installed registry against the current capability
96
+ * universe (evidence) and across plugins. Returns the entries that may activate
97
+ * plus per-surface quarantine records. Disabled entries/surfaces are skipped
98
+ * (not an error).
99
+ */
100
+ export function validateSessionBundles(
101
+ entries: readonly SkcPluginRegistryEntry[],
102
+ evidence: SessionCapabilityEvidence = {},
103
+ preQuarantined: readonly SessionQuarantine[] = [],
104
+ ): SessionValidationResult {
105
+ const quarantine: SessionQuarantine[] = [...preQuarantined];
106
+ const quarantinedPlugins = new Set(preQuarantined.map(q => q.plugin));
107
+
108
+ const seenTools = new Set<string>(evidence.toolNames ?? []);
109
+ const seenMcps = new Set<string>(evidence.mcpNames ?? []);
110
+ const seenHooks = new Set<string>(evidence.hookKeys ?? []);
111
+ const seenAppendices = new Set<string>(evidence.appendixIds ?? []);
112
+
113
+ const active: SkcPluginRegistryEntry[] = [];
114
+ for (const entry of entries) {
115
+ if (!entry.enabled) continue; // user-disabled, not an error
116
+ if (quarantinedPlugins.has(entry.name)) continue;
117
+ const surfaces = activeSurfaceIds(entry);
118
+ let collided = false;
119
+ const recordCollision = (surfaceId: string, what: string): void => {
120
+ collided = true;
121
+ quarantine.push({
122
+ plugin: entry.name,
123
+ surfaceId,
124
+ code: "session_collision",
125
+ message: `${what} collides with an existing capability/plugin; fail-closed (no shadowing)`,
126
+ });
127
+ };
128
+ for (const t of surfaces.tools) if (seenTools.has(t.name)) recordCollision(t.id, `tool "${t.name}"`);
129
+ for (const m of surfaces.mcps) if (seenMcps.has(m.name)) recordCollision(m.id, `mcp "${m.name}"`);
130
+ for (const h of surfaces.hooks) if (seenHooks.has(h.id)) recordCollision(h.id, `hook "${h.id}"`);
131
+ for (const a of surfaces.appendices) if (seenAppendices.has(a.id)) recordCollision(a.id, `appendix "${a.id}"`);
132
+
133
+ if (collided) {
134
+ // Fail-closed for the whole plugin entry so partial activation cannot
135
+ // leave a half-applied bundle.
136
+ continue;
137
+ }
138
+ // Reserve this plugin's surfaces so a later plugin colliding with it is
139
+ // also caught (deterministic order => earlier plugin wins reservation).
140
+ for (const t of surfaces.tools) seenTools.add(t.name);
141
+ for (const m of surfaces.mcps) seenMcps.add(m.name);
142
+ for (const h of surfaces.hooks) seenHooks.add(h.id);
143
+ for (const a of surfaces.appendices) seenAppendices.add(a.id);
144
+ active.push(entry);
145
+ }
146
+ return { active, quarantine };
147
+ }
@@ -19,12 +19,62 @@ export const SKC_AGENT_SUBSKILL_PHASES: Record<SkcSubskillParentAgent, string[]>
19
19
  critic: ["prompt"],
20
20
  };
21
21
 
22
+ export interface SkcPluginToolManifestEntry {
23
+ name: string;
24
+ path: string;
25
+ description?: string;
26
+ sha256?: string;
27
+ /**
28
+ * "always-on" object entries are activated for the whole session; legacy
29
+ * string shorthand stays "subskill"-scoped and is only attached to subskill
30
+ * bindings (never registered as an always-on tool surface).
31
+ */
32
+ surface: "subskill" | "always-on";
33
+ }
34
+
35
+ export interface SkcPluginHookManifestEntry {
36
+ name: string;
37
+ event: string;
38
+ target?: string;
39
+ phase?: "before" | "after";
40
+ path: string;
41
+ sha256?: string;
42
+ }
43
+
44
+ export type SkcPluginMcpTransport = "stdio" | "http" | "sse";
45
+
46
+ export interface SkcPluginMcpManifestEntry {
47
+ name: string;
48
+ transport: SkcPluginMcpTransport;
49
+ command?: string;
50
+ args?: string[];
51
+ cwd?: string;
52
+ url?: string;
53
+ headers?: Record<string, string>;
54
+ sha256?: string;
55
+ }
56
+
57
+ export interface SkcPluginAppendixManifestEntry {
58
+ name: string;
59
+ path?: string;
60
+ content?: string;
61
+ sha256?: string;
62
+ }
63
+
64
+ export interface SkcPluginAgentAppendixManifestEntry extends SkcPluginAppendixManifestEntry {
65
+ agent: SkcSubskillParentAgent;
66
+ }
67
+
22
68
  export interface SkcPluginManifest {
23
69
  name: string;
24
70
  version: string;
25
71
  kind: "sayknow-cli-plugin";
26
72
  subskills: string[];
27
- tools: string[];
73
+ tools: SkcPluginToolManifestEntry[];
74
+ hooks: SkcPluginHookManifestEntry[];
75
+ mcps: SkcPluginMcpManifestEntry[];
76
+ systemAppendix: SkcPluginAppendixManifestEntry[];
77
+ agentAppendix: SkcPluginAgentAppendixManifestEntry[];
28
78
  }
29
79
 
30
80
  export interface SubskillFrontmatter {
@@ -76,15 +126,33 @@ export interface LoadedSkcPlugin {
76
126
  }
77
127
 
78
128
  export type SkcPluginLoadErrorCode =
129
+ // Parse-time
79
130
  | "forbidden_surface"
80
131
  | "invalid_manifest"
132
+ | "invalid_kind"
133
+ | "unsupported_surface"
134
+ // Compile-time
81
135
  | "invalid_frontmatter"
82
136
  | "invalid_parent"
83
137
  | "invalid_phase"
138
+ | "missing_file"
139
+ | "hash_mismatch"
140
+ | "invalid_appendix"
141
+ | "invalid_hook"
142
+ | "invalid_mcp"
143
+ // Install-time
84
144
  | "duplicate_arg"
85
145
  | "duplicate_parent_phase"
86
- | "missing_file"
87
- | "invalid_kind";
146
+ | "duplicate_tool"
147
+ | "duplicate_hook"
148
+ | "duplicate_mcp"
149
+ | "duplicate_appendix"
150
+ | "security_policy"
151
+ | "install_conflict"
152
+ // Session-start / runtime
153
+ | "session_collision"
154
+ | "runtime_mismatch"
155
+ | "quarantined_surface";
88
156
 
89
157
  export class SkcPluginLoadError extends Error {
90
158
  readonly code: SkcPluginLoadErrorCode;
@@ -95,3 +163,131 @@ export class SkcPluginLoadError extends Error {
95
163
  this.code = code;
96
164
  }
97
165
  }
166
+
167
+ export type SkcPluginScope = "user" | "project";
168
+
169
+ export type SkcPluginSourceKind = "path" | "git" | "tarball";
170
+
171
+ export interface SkcPluginCopiedFile {
172
+ relativePath: string;
173
+ sha256: string;
174
+ bytes: number;
175
+ }
176
+
177
+ export interface NormalizedSubskillSurface {
178
+ extensionId: string;
179
+ name: string;
180
+ description: string;
181
+ parent: string;
182
+ phase: string;
183
+ activationArg: string;
184
+ relativePath: string;
185
+ sha256: string;
186
+ }
187
+
188
+ export interface NormalizedToolSurface {
189
+ extensionId: string;
190
+ name: string;
191
+ relativePath: string;
192
+ sha256: string;
193
+ description?: string;
194
+ }
195
+
196
+ export interface NormalizedHookSurface {
197
+ extensionId: string;
198
+ name: string;
199
+ event: string;
200
+ target?: string;
201
+ phase?: "before" | "after";
202
+ relativePath: string;
203
+ sha256: string;
204
+ }
205
+
206
+ export interface NormalizedMcpSurface {
207
+ extensionId: string;
208
+ name: string;
209
+ transport: SkcPluginMcpTransport;
210
+ configHash: string;
211
+ config: SkcPluginMcpManifestEntry;
212
+ }
213
+
214
+ export interface NormalizedAppendixSurface {
215
+ extensionId: string;
216
+ name: string;
217
+ relativePath?: string;
218
+ /** Inline appendix body (when the manifest used `content` instead of `path`). */
219
+ content?: string;
220
+ contentHash: string;
221
+ bytes: number;
222
+ }
223
+
224
+ export interface NormalizedAgentAppendixSurface extends NormalizedAppendixSurface {
225
+ agent: SkcSubskillParentAgent;
226
+ }
227
+
228
+ export interface NormalizedSkcPluginSurfaces {
229
+ subskills: NormalizedSubskillSurface[];
230
+ tools: NormalizedToolSurface[];
231
+ hooks: NormalizedHookSurface[];
232
+ mcps: NormalizedMcpSurface[];
233
+ systemAppendices: NormalizedAppendixSurface[];
234
+ agentAppendices: NormalizedAgentAppendixSurface[];
235
+ }
236
+
237
+ /**
238
+ * Result of the pure compile step. Computed from manifest, frontmatter, and
239
+ * declared files read as bytes only — never by importing plugin code.
240
+ */
241
+ export interface NormalizedSkcPluginBundle {
242
+ name: string;
243
+ version: string;
244
+ root: string;
245
+ manifestPath: string;
246
+ manifestHash: string;
247
+ surfaces: NormalizedSkcPluginSurfaces;
248
+ files: SkcPluginCopiedFile[];
249
+ }
250
+
251
+ export interface SkcPluginQuarantineEntry {
252
+ surfaceId: string;
253
+ code: SkcPluginLoadErrorCode;
254
+ message: string;
255
+ detectedAt: string;
256
+ }
257
+
258
+ export interface SkcPluginRegistrySource {
259
+ kind: SkcPluginSourceKind;
260
+ uri: string;
261
+ ref?: string;
262
+ sha?: string;
263
+ resolvedAt: string;
264
+ }
265
+
266
+ export interface SkcPluginRegistryEntry {
267
+ name: string;
268
+ version: string;
269
+ scope: SkcPluginScope;
270
+ enabled: boolean;
271
+ pluginRoot: string;
272
+ manifestPath: string;
273
+ manifestHash: string;
274
+ source: SkcPluginRegistrySource;
275
+ installedAt: string;
276
+ updatedAt: string;
277
+ copiedFiles: SkcPluginCopiedFile[];
278
+ surfaces: NormalizedSkcPluginSurfaces;
279
+ disabledSurfaceIds: string[];
280
+ quarantine?: SkcPluginQuarantineEntry[];
281
+ }
282
+
283
+ export interface SkcPluginRegistry {
284
+ version: 1;
285
+ scope: SkcPluginScope;
286
+ plugins: SkcPluginRegistryEntry[];
287
+ }
288
+
289
+ /**
290
+ * Stable identifiers for plugin-contributed surfaces used by observability,
291
+ * disabledSurfaceIds, and quarantine bookkeeping.
292
+ */
293
+ export type SkcPluginSurfaceExtensionId = string;