@intentic/extension-manifest 1.176.3

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 (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/bundle.d.ts +4 -0
  4. package/dist/bundle.d.ts.map +1 -0
  5. package/dist/bundle.js +22 -0
  6. package/dist/bundle.js.map +1 -0
  7. package/dist/contribution-point.d.ts +7 -0
  8. package/dist/contribution-point.d.ts.map +1 -0
  9. package/dist/contribution-point.js +2 -0
  10. package/dist/contribution-point.js.map +1 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +8 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/json-schema.d.ts +4 -0
  16. package/dist/json-schema.d.ts.map +1 -0
  17. package/dist/json-schema.js +33 -0
  18. package/dist/json-schema.js.map +1 -0
  19. package/dist/manifest.d.ts +300 -0
  20. package/dist/manifest.d.ts.map +1 -0
  21. package/dist/manifest.js +46 -0
  22. package/dist/manifest.js.map +1 -0
  23. package/dist/mark.d.ts +6 -0
  24. package/dist/mark.d.ts.map +1 -0
  25. package/dist/mark.js +12 -0
  26. package/dist/mark.js.map +1 -0
  27. package/dist/permissions.d.ts +2 -0
  28. package/dist/permissions.d.ts.map +1 -0
  29. package/dist/permissions.js +21 -0
  30. package/dist/permissions.js.map +1 -0
  31. package/dist/points/agent.d.ts +13 -0
  32. package/dist/points/agent.d.ts.map +1 -0
  33. package/dist/points/agent.js +10 -0
  34. package/dist/points/agent.js.map +1 -0
  35. package/dist/points/automation-templates.d.ts +67 -0
  36. package/dist/points/automation-templates.d.ts.map +1 -0
  37. package/dist/points/automation-templates.js +47 -0
  38. package/dist/points/automation-templates.js.map +1 -0
  39. package/dist/points/bin.d.ts +7 -0
  40. package/dist/points/bin.d.ts.map +1 -0
  41. package/dist/points/bin.js +10 -0
  42. package/dist/points/bin.js.map +1 -0
  43. package/dist/points/capabilities.d.ts +352 -0
  44. package/dist/points/capabilities.d.ts.map +1 -0
  45. package/dist/points/capabilities.js +131 -0
  46. package/dist/points/capabilities.js.map +1 -0
  47. package/dist/points/commands.d.ts +21 -0
  48. package/dist/points/commands.d.ts.map +1 -0
  49. package/dist/points/commands.js +23 -0
  50. package/dist/points/commands.js.map +1 -0
  51. package/dist/points/documents.d.ts +15 -0
  52. package/dist/points/documents.d.ts.map +1 -0
  53. package/dist/points/documents.js +14 -0
  54. package/dist/points/documents.js.map +1 -0
  55. package/dist/points/environment.d.ts +13 -0
  56. package/dist/points/environment.d.ts.map +1 -0
  57. package/dist/points/environment.js +14 -0
  58. package/dist/points/environment.js.map +1 -0
  59. package/dist/points/files.d.ts +15 -0
  60. package/dist/points/files.d.ts.map +1 -0
  61. package/dist/points/files.js +20 -0
  62. package/dist/points/files.js.map +1 -0
  63. package/dist/points/index.d.ts +609 -0
  64. package/dist/points/index.d.ts.map +1 -0
  65. package/dist/points/index.js +44 -0
  66. package/dist/points/index.js.map +1 -0
  67. package/dist/points/listener.d.ts +51 -0
  68. package/dist/points/listener.d.ts.map +1 -0
  69. package/dist/points/listener.js +44 -0
  70. package/dist/points/listener.js.map +1 -0
  71. package/dist/points/processes.d.ts +23 -0
  72. package/dist/points/processes.d.ts.map +1 -0
  73. package/dist/points/processes.js +15 -0
  74. package/dist/points/processes.js.map +1 -0
  75. package/dist/points/settings.d.ts +37 -0
  76. package/dist/points/settings.d.ts.map +1 -0
  77. package/dist/points/settings.js +24 -0
  78. package/dist/points/settings.js.map +1 -0
  79. package/dist/points/viewers.d.ts +25 -0
  80. package/dist/points/viewers.d.ts.map +1 -0
  81. package/dist/points/viewers.js +17 -0
  82. package/dist/points/viewers.js.map +1 -0
  83. package/dist/points/views.d.ts +27 -0
  84. package/dist/points/views.d.ts.map +1 -0
  85. package/dist/points/views.js +18 -0
  86. package/dist/points/views.js.map +1 -0
  87. package/dist/powers-diff.d.ts +8 -0
  88. package/dist/powers-diff.d.ts.map +1 -0
  89. package/dist/powers-diff.js +77 -0
  90. package/dist/powers-diff.js.map +1 -0
  91. package/intentic-extension.schema.json +1341 -0
  92. package/package.json +49 -0
  93. package/src/bundle.ts +41 -0
  94. package/src/contribution-point.ts +26 -0
  95. package/src/index.ts +7 -0
  96. package/src/json-schema.ts +62 -0
  97. package/src/manifest.ts +104 -0
  98. package/src/mark.ts +34 -0
  99. package/src/permissions.ts +36 -0
  100. package/src/points/agent.ts +17 -0
  101. package/src/points/automation-templates.ts +84 -0
  102. package/src/points/bin.ts +15 -0
  103. package/src/points/capabilities.ts +271 -0
  104. package/src/points/commands.ts +44 -0
  105. package/src/points/documents.ts +31 -0
  106. package/src/points/environment.ts +24 -0
  107. package/src/points/files.ts +54 -0
  108. package/src/points/index.ts +73 -0
  109. package/src/points/listener.ts +81 -0
  110. package/src/points/processes.ts +20 -0
  111. package/src/points/settings.ts +31 -0
  112. package/src/points/viewers.ts +36 -0
  113. package/src/points/views.ts +33 -0
  114. package/src/powers-diff.ts +100 -0
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@intentic/extension-manifest",
3
+ "version": "1.176.3",
4
+ "description": "What an intentic extension DECLARES — the intentic-extension.json schema and the sandbox-route allowlist rule",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/intentic/intentic.git",
10
+ "directory": "_sandbox/extension-manifest"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "intentic-extension.schema.json"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": {
25
+ "@intentic/src": "./src/index.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "require": {
29
+ "@intentic/src": "./src/index.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "tslib": "2.8.1",
36
+ "zod": "4.4.3",
37
+ "@intentic/base": "1.176.3"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "24.13.2",
41
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
42
+ "@intentic/tsconfig": "0.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsgo",
46
+ "watch": "tsgo --build --watch",
47
+ "schema": "tsgo && node scripts/write-schema.mjs"
48
+ }
49
+ }
package/src/bundle.ts ADDED
@@ -0,0 +1,41 @@
1
+ /* WHAT A PUBLISHED BUNDLE MAY IMPORT — the loader's side of the manifest contract, stated where both sides can
2
+ * read it. The host fetches an extension's entry bytes and imports them from a blob: URL, which has two hard
3
+ * consequences: a relative import resolves against a blob: URL that was never created (a 404 for a file that
4
+ * exists on disk), and a bare specifier resolves only if the shell's import map publishes it. Both failures are
5
+ * invisible to the author — their own workspace loads the directory live — and fatal for every installer.
6
+ *
7
+ * It lives HERE, beside the manifest schema, because two independent judges have to agree on it: the daemon's
8
+ * readiness check (before an author publishes) and the registry scanner (re-deriving the same answer cold, at
9
+ * the pinned sha, every night). Two hand-rolled copies of this rule would drift exactly the way the manifest
10
+ * schema and its published copy once did. */
11
+
12
+ // What the shell's import map publishes to a bundle (the web app's hostModules.ts is the runtime source of
13
+ // truth; its shim generator and this list are both asserted against the same module names).
14
+ export const HOST_PUBLISHED_SPECIFIERS = ["vue", "@intentic/extension-api", "@intentic/extension-ui", "@tanstack/vue-query"] as const;
15
+
16
+ // Every specifier a single-file ESM bundle names: static imports, re-exports, bare side-effect imports, and
17
+ // dynamic import(). A bundle is one file by contract, so a regex over its text is the right instrument — there
18
+ // is no module graph to walk.
19
+ export const bundleSpecifiers = (source: string): string[] => [
20
+ ...new Set([
21
+ ...[...source.matchAll(/(?:^|\n)\s*(?:import|export)[^;\n]*?from\s*["'`]([^"'`]+)["'`]/gu)].map((match) => match[1] ?? ""),
22
+ ...[...source.matchAll(/\bimport\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gu)].map((match) => match[1] ?? ""),
23
+ ...[...source.matchAll(/(?:^|\n)\s*import\s*["'`]([^"'`]+)["'`]/gu)].map((match) => match[1] ?? ""),
24
+ ]),
25
+ ];
26
+
27
+ /* Why this bundle cannot load, or undefined when it can. One sentence naming the offending specifiers, because
28
+ * both callers surface it verbatim: the readiness row to the author, the registry facts to anyone browsing. */
29
+ export const bundleProblem = (source: string): string | undefined => {
30
+ const specifiers = bundleSpecifiers(source);
31
+ const relative = specifiers.filter((specifier) => specifier.startsWith(".") || specifier.startsWith("/"));
32
+ if (relative.length > 0) {
33
+ return `imports a second file (${relative.join(", ")}) — a bundle is imported from a blob URL, so nothing relative to it can resolve`;
34
+ }
35
+ const published = new Set<string>(HOST_PUBLISHED_SPECIFIERS);
36
+ const unpublished = specifiers.filter((specifier) => !published.has(specifier));
37
+ if (unpublished.length > 0) {
38
+ return `imports ${unpublished.join(", ")}, which the host does not publish — bundle it in, or use one of: ${HOST_PUBLISHED_SPECIFIERS.join(", ")}`;
39
+ }
40
+ return undefined;
41
+ };
@@ -0,0 +1,26 @@
1
+ import type { z } from "zod";
2
+
3
+ /* A CONTRIBUTION POINT, DEFINED ONCE — its key, its shape, and the sentence that explains it to the person
4
+ * writing the manifest, in one object.
5
+ *
6
+ * The three used to live apart, and only two of them lived anywhere at all. The key and the shape sat in one
7
+ * file that every feature had to edit to add anything, in two places (the schema, then the key on `contributes`);
8
+ * the explanation sat in a `//` comment above it, where the extension author — the only reader it was written
9
+ * for — could never see it. So the manifest was a thing you wrote by copying another extension's and guessing,
10
+ * with a misspelt contribution point dropped in silence rather than named.
11
+ *
12
+ * Binding the description to the schema is what changes that: it rides `z.describe`, so it reaches the generated
13
+ * authoring schema (json-schema.ts) and comes back as hover text in the author's editor. The prose that is for
14
+ * MAINTAINERS — why a point exists, what it replaced, what it deliberately does not do — stays a comment in the
15
+ * point's own file, because it is not what someone filling in a field needs to read. */
16
+ export interface ContributionPoint<Name extends string = string, Schema extends z.ZodType = z.ZodType> {
17
+ // The key under `contributes` in intentic-extension.json.
18
+ readonly name: Name;
19
+ /* Written for the author, in the second person, and kept to what they must decide: what declaring this gets
20
+ * them, and what the host does with it. It lands verbatim in editor hover text, so a paragraph of rationale
21
+ * here is a paragraph in a tooltip. */
22
+ readonly description: string;
23
+ // The value shape under that key — an array for a point that takes many entries, the entry itself for a
24
+ // point that takes one. `contributes` makes every one of them optional; a point is never required.
25
+ readonly schema: Schema;
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./bundle.js";
2
+ export * from "./contribution-point.js";
3
+ export * from "./json-schema.js";
4
+ export * from "./manifest.js";
5
+ export * from "./permissions.js";
6
+ export * from "./points/index.js";
7
+ export * from "./powers-diff.js";
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import { ExtensionManifestSchema } from "./manifest.js";
3
+
4
+ /* THE AUTHORING SCHEMA — what an editor reads to help someone write `intentic-extension.json`.
5
+ *
6
+ * A manifest was previously written by copying another extension's and guessing. Nothing told the author what a
7
+ * field meant, because every explanation lived in a `//` comment in this package; and nothing told them when
8
+ * they got one wrong, because zod STRIPS unknown keys at every level rather than refusing them. A misspelt
9
+ * `viewers` was not an error — it was a viewer that never appeared, discovered at install, with the manifest
10
+ * parsing perfectly.
11
+ *
12
+ * So: the same points, emitted as JSON Schema, with the descriptions that ride each one (contribution-point.ts)
13
+ * arriving as hover text. Editors resolve `$schema` and give completion, documentation and a red squiggle on a
14
+ * key nothing declares.
15
+ *
16
+ * STRICT HERE, LENIENT AT RUNTIME, and the asymmetry is the point. Authoring is where an unknown key is a typo
17
+ * and should be shouted about. Runtime is where an unknown key is a manifest written for a NEWER host, which an
18
+ * older daemon must go on installing with the point it doesn't understand ignored — refusing it outright would
19
+ * make every addition to this list a breaking change. */
20
+
21
+ // Where the published copy answers, so `$schema` in a manifest resolves for an author who has installed nothing.
22
+ export const MANIFEST_SCHEMA_URL = "https://intentic.dev/intentic-extension.schema.json";
23
+
24
+ /* `additionalProperties: false` on every object node, so a key nothing declares is flagged where it is typed.
25
+ *
26
+ * Skips a node that already carries `additionalProperties` — that is a `z.record`, whose whole shape is "any key,
27
+ * this value" (a cli capability's `env`), and pinning it closed would reject every entry it exists to accept. */
28
+ const closeToUnknownKeys = (node: unknown): void => {
29
+ if (Array.isArray(node)) {
30
+ for (const item of node) {
31
+ closeToUnknownKeys(item);
32
+ }
33
+ return;
34
+ }
35
+ if (typeof node !== "object" || node === null) {
36
+ return;
37
+ }
38
+ const schema = node as Record<string, unknown>;
39
+ if (schema["type"] === "object" && schema["properties"] !== undefined && schema["additionalProperties"] === undefined) {
40
+ schema["additionalProperties"] = false;
41
+ }
42
+ for (const value of Object.values(schema)) {
43
+ closeToUnknownKeys(value);
44
+ }
45
+ };
46
+
47
+ /* The manifest schema as JSON Schema. `io: "input"` because this describes what an author WRITES — the shape
48
+ * going in, before any refinement or default has been applied to it. */
49
+ export const manifestJsonSchema = (): Record<string, unknown> => {
50
+ const schema = z.toJSONSchema(ExtensionManifestSchema, { unrepresentable: "any", io: "input" }) as Record<string, unknown>;
51
+ closeToUnknownKeys(schema);
52
+ return {
53
+ ...schema,
54
+ $id: MANIFEST_SCHEMA_URL,
55
+ title: "intentic extension manifest",
56
+ description: "What an intentic extension declares: who it is, which host it needs, what code it ships, and what it contributes.",
57
+ };
58
+ };
59
+
60
+ // The committed file's exact bytes, so the generator and the check that guards it cannot disagree about
61
+ // formatting — four spaces and a trailing newline, the repo's shape for a committed generated document.
62
+ export const serializeManifestJsonSchema = (schema: Record<string, unknown>): string => `${JSON.stringify(schema, undefined, 4)}\n`;
@@ -0,0 +1,104 @@
1
+ import { z } from "zod";
2
+ import { MARK_FIELDS } from "./mark.js";
3
+ import { contributesSchema } from "./points/index.js";
4
+
5
+ /* The extension manifest: `intentic-extension.json` at the extension repo root (deliberately NOT inside
6
+ * .claude-plugin/ — that directory is Claude Code's namespace with its own semantics). The manifest is the
7
+ * approval surface: the install dialog shows exactly these declared contributions before the owner confirms,
8
+ * and the host refuses runtime registrations (views, commands) whose ids the approved manifest never declared.
9
+ *
10
+ * This file is the ENVELOPE only — who the extension is, which host it needs, what code it ships, how far it
11
+ * may reach. What it may CONTRIBUTE is one file per contribution point under points/, assembled here; see
12
+ * contribution-point.ts for why the description travels with the schema instead of sitting in a comment. */
13
+
14
+ export const ExtensionManifestSchema = z.object({
15
+ /* The authoring schema this manifest is written against — editors read it and give the author completion,
16
+ * hover text and a red squiggle on a misspelt key. Declared so it survives the parse rather than being
17
+ * silently stripped, which is what would otherwise happen to the one field an author is most likely to add
18
+ * by hand. Nothing at runtime reads it. */
19
+ $schema: z.string().optional().describe("The authoring schema, for editor completion and validation. Nothing at runtime reads it."),
20
+ publisher: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
21
+ name: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
22
+ // The extension's own semver — display/identity only; the installed code identity is the pinned commit sha.
23
+ version: z.string().min(1).describe("Your own semver — display and identity only. The installed code's identity is the pinned commit sha."),
24
+ /* The section this extension sits under in the Sandbox hub's Extensions tab — a grouping by what it is FOR,
25
+ * declared because it cannot be derived. Nine of the first-party extensions contribute a rail tile, so a
26
+ * grouping read off `contributes` puts more than half the list in one section and says nothing about any of
27
+ * them. Deliberately a loose string, exactly like a connector's `catalog.category`: the vocabulary belongs
28
+ * to the surface that renders it (extensionCategories.ts in the web app), and an extension declaring a
29
+ * section this app has never heard of lands in "Other" rather than failing to install. */
30
+ category: z
31
+ .string()
32
+ .min(1)
33
+ .optional()
34
+ .describe(
35
+ "Which section of the Extensions tab this sits under — a grouping by what it is FOR, which cannot be derived from what it contributes. A section this app has never heard of lands in “Other” rather than failing to install.",
36
+ ),
37
+ /* What the extension is drawn as wherever it is LISTED rather than used — the Extensions tab, a registry
38
+ * being browsed, the gallery. Deliberately here and not on a view: `Activation.icon` is the glyph of one
39
+ * rail tile, it only exists once the extension's code has activated in this browser, and nine of the
40
+ * first-party extensions register no view at all. An extension that is switched off, daemon-only, or not
41
+ * yet installed still has to look like something. See MARK_FIELDS. */
42
+ ...MARK_FIELDS,
43
+ // Semver range over the host's extension API version (extensionApiVersion) — checked before activation.
44
+ engines: z
45
+ .object({ intentic: z.string().min(1) })
46
+ .describe("A semver range over the host's extension API version, checked before your code is activated."),
47
+ // Repo-relative path of the prebuilt single-file ESM bundle (built with `vue` and `@intentic/extension-api`
48
+ // as externals); absent ⇒ an extension with no UI entry.
49
+ entry: z
50
+ .string()
51
+ .min(1)
52
+ .refine((value) => !value.split("/").includes(".."), { message: "entry must stay inside the checkout" })
53
+ .optional()
54
+ .describe(
55
+ "Repo-relative path of your prebuilt single-file ESM bundle, built with `vue` and `@intentic/extension-api` as externals. Absent ⇒ an extension with no UI.",
56
+ ),
57
+ /* Repo-relative path of the prebuilt single-file node ESM SERVER bundle — the extension's BACKEND half,
58
+ * exporting `activateServer(api, context)`. Loaded by the daemon's backend host (a separate supervised
59
+ * process, so a toggle or a live edit is a host restart rather than a daemon death) and served under the
60
+ * extension's own route namespace `/x/<id>/…`, which the daemon proxies. Self-contained by construction:
61
+ * the host provides no import map and the baked checkout has no node_modules, so everything but node
62
+ * builtins must be bundled in. Absent ⇒ the extension has no backend. */
63
+ server: z
64
+ .string()
65
+ .min(1)
66
+ .refine((value) => !value.split("/").includes(".."), { message: "server must stay inside the checkout" })
67
+ .optional()
68
+ .describe(
69
+ "Repo-relative path of your prebuilt single-file node ESM server bundle, exporting `activateServer`. Served under your own route namespace, which the daemon proxies. Nothing is provided at runtime but node builtins, so bundle everything else in. Absent ⇒ no backend.",
70
+ ),
71
+ // Declared reach, both halves in one grammar — "<METHOD> <path-glob>" where `*` matches one path segment
72
+ // (e.g. "GET /panels", "POST /panels/*/start") — so the install dialog, the gate and the usage ledger read
73
+ // one vocabulary.
74
+ // sandbox — the daemon routes the UI half may call through api.sandbox (the host refuses undeclared
75
+ // ones). An extension's OWN namespace `/x/<its id>/…` needs no entry: its backend is its own.
76
+ // daemon — the daemon routes the SERVER half may call through api.daemon, enforced by the daemon's
77
+ // extension-token grant. Separate from `sandbox` because the halves run as different
78
+ // principals: the UI acts with the owner's session, the backend with a minted per-extension
79
+ // token, and a grant to one must never quietly widen the other.
80
+ // Absent (or an absent key) ⇒ that half makes no daemon calls.
81
+ permissions: z
82
+ .object({
83
+ sandbox: z
84
+ .array(z.string())
85
+ .optional()
86
+ .describe("Daemon routes your UI half may call. Your own backend namespace needs no entry — its backend is your own code."),
87
+ daemon: z
88
+ .array(z.string())
89
+ .optional()
90
+ .describe(
91
+ "Daemon routes your SERVER half may call. Separate from `sandbox` because the two halves run as different principals — the UI as the owner's session, the backend as a minted per-extension token — so a grant to one must never quietly widen the other.",
92
+ ),
93
+ })
94
+ .optional()
95
+ .describe(
96
+ 'How far this extension may reach into the daemon, as "<METHOD> <path-glob>" entries where `*` matches one path segment — e.g. "GET /panels", "POST /panels/*/start". The install dialog shows these, the host refuses anything undeclared, and the usage ledger records which were actually earned.',
97
+ ),
98
+ contributes: contributesSchema.optional(),
99
+ });
100
+ export type ExtensionManifest = z.infer<typeof ExtensionManifestSchema>;
101
+
102
+ // The extension's identity everywhere (capability entries, /ext routes, settings namespaces) — derived, never
103
+ // declared, so it can't contradict the publisher/name the install dialog showed.
104
+ export const extensionIdOf = (manifest: Pick<ExtensionManifest, "publisher" | "name">): string => `${manifest.publisher}.${manifest.name}`;
package/src/mark.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+
3
+ /* HOW SOMETHING LOOKS BEFORE ANY OF ITS CODE RUNS — the mark a capability card and an extension are drawn
4
+ * with, in ONE shape because one component draws both (<BrandMark>) and a second copy of these two fields is a
5
+ * second answer to what happens when a slug 404s.
6
+ *
7
+ * Two tiers, and neither is required. `logo` is a simple-icons slug fetched from a CDN: exactly right for a
8
+ * card standing in for somebody else's product (GitHub, Postgres, Slack), useless for the many things that
9
+ * have no brand in that set, and unreachable in an offline sandbox — so it can never be the only tier. `icon`
10
+ * is a name from the host's own bundled vocabulary, which ships in the image, follows the theme and costs no
11
+ * request; it is what actually carries a first-party extension. What declares neither is drawn as its
12
+ * initials, so no row is ever blank and no author is obliged to have a brand.
13
+ *
14
+ * A slug that fails to load and an icon name this build has never heard of both fall to the tier BELOW rather
15
+ * than to a hole — the rule the rail already applies to Activation.icon, here for the surfaces that must draw
16
+ * an extension whose code is not running: one that is switched off, one that is daemon-only, one being read
17
+ * about in a registry before it is installed at all. */
18
+ export const MARK_FIELDS = {
19
+ // A simple-icons slug (https://cdn.simpleicons.org/<slug>). A "/<hex>" suffix forces a colour for marks
20
+ // that vanish against the surface they land on (github's near-black).
21
+ logo: z
22
+ .string()
23
+ .optional()
24
+ .describe(
25
+ 'A simple-icons slug, fetched from a CDN — right for standing in for somebody else\'s product. Add a "/<hex>" suffix to force a colour for a mark that vanishes against the surface it lands on. Unreachable in an offline sandbox, so it falls back to `icon`, then to initials.',
26
+ ),
27
+ // A name from the host's icon set (@intentic/ui IconName), drawn when no simple-icons slug fits.
28
+ icon: z
29
+ .string()
30
+ .optional()
31
+ .describe(
32
+ "A name from the host's own icon set, drawn when no simple-icons slug fits. It ships in the image, follows the theme and costs no request — what actually carries a first-party extension. An unknown name falls back to initials rather than to a hole.",
33
+ ),
34
+ };
@@ -0,0 +1,36 @@
1
+ /* The sandbox-route permission model. An extension declares in its manifest exactly which daemon routes it may
2
+ * reach through `api.sandbox.request/json`, as "<METHOD> <path-glob>" strings where `*` matches exactly one path
3
+ * segment. The host matches every call against these and refuses an undeclared route — so an extension's backend
4
+ * reach is explicit, diffable, and reviewable rather than an ambient client to the whole daemon. */
5
+
6
+ const escapeRegExp = (literal: string): string => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
+
8
+ interface CompiledRoute {
9
+ readonly method: string;
10
+ readonly test: (path: string) => boolean;
11
+ }
12
+
13
+ // "<METHOD> <path-glob>" → a method + an anchored path matcher. Each `*` in the glob matches one path segment
14
+ // ([^/]+); everything else is literal. Throws on a malformed entry so a bad manifest fails loudly at load.
15
+ const compile = (entry: string): CompiledRoute => {
16
+ const spaceIndex = entry.indexOf(" ");
17
+ if (spaceIndex < 0) {
18
+ throw new Error(`invalid sandbox permission "${entry}" — expected "<METHOD> <path-glob>", e.g. "GET /panels"`);
19
+ }
20
+ const method = entry.slice(0, spaceIndex).trim().toUpperCase();
21
+ const glob = entry.slice(spaceIndex + 1).trim();
22
+ const source = `^${glob.split("*").map(escapeRegExp).join("[^/]+")}$`;
23
+ const regex = new RegExp(source);
24
+ return { method, test: (path) => regex.test(path) };
25
+ };
26
+
27
+ // Whether `method path` is covered by any of the declared permissions. The query string is ignored (routes are
28
+ // matched on path only), and the method is compared case-insensitively.
29
+ export const sandboxRouteAllowed = (permissions: readonly string[], method: string, path: string): boolean => {
30
+ const route = path.split("?")[0] ?? path;
31
+ const wanted = method.toUpperCase();
32
+ return permissions.some((entry) => {
33
+ const compiled = compile(entry);
34
+ return compiled.method === wanted && compiled.test(route);
35
+ });
36
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ // "This checkout is ALSO a Claude Code plugin": the daemon hands the directory to the Agent SDK's plugin
5
+ // loader, which reads skills/agents/hooks/commands/.mcp.json each turn — the daemon never parses plugin
6
+ // internals.
7
+ export const AgentContributionSchema = z.object({
8
+ path: z.string().optional().describe("Relative to the extension checkout. Absent ⇒ the checkout root."),
9
+ });
10
+ export type AgentContribution = z.infer<typeof AgentContributionSchema>;
11
+
12
+ export const agentPoint = {
13
+ name: "agent",
14
+ description:
15
+ "Declare that this checkout is also a Claude Code plugin, so the agent picks up its skills, agents, hooks, commands and MCP servers each turn. The daemon hands the directory to the plugin loader and never parses what is in it.",
16
+ schema: AgentContributionSchema,
17
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ /* A STARTING POINT in the automation composer — a trigger, a prompt written for that trigger's payload, and
5
+ * whatever guard or hold makes it safe to leave on. Pure prefill: creating one makes an ordinary automation and
6
+ * the daemon knows nothing about templates afterwards.
7
+ *
8
+ * IT LIVES WITH THE AREA THAT KNOWS THE SERVICE, which is the point of it being a contribution at all. The
9
+ * automation surface used to carry every one of these — Komodo's, Sentry's, Stripe's, CI's, the chore book's —
10
+ * so a pack that gained something worth reacting to could not say so without an edit to a surface it has
11
+ * nothing to do with. A template declared here appears when the pack is installed and its capability connected,
12
+ * and disappears with it.
13
+ *
14
+ * The daemon validates each one against the real trigger schema when it builds the catalogue and drops what
15
+ * does not parse, so a template can never offer a trigger that `upsert` would refuse. */
16
+ export const AutomationTemplateContributionSchema = z.object({
17
+ // Prefills the automation name, and is what "does one of these exist already" is asked by — so it must be
18
+ // spelled as an automation id, not as prose.
19
+ id: z
20
+ .string()
21
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/)
22
+ .describe('Prefills the automation name, and is what "does one of these exist already" is asked by — so spell it as an id, not as prose.'),
23
+ title: z.string().min(1),
24
+ logo: z.string().min(1).optional().describe("A simple-icons slug for the card."),
25
+ icon: z.string().min(1).optional().describe("A name from the host's icon set, drawn when no simple-icons slug fits."),
26
+ // Capability providers that make this template WORK — any one connected is enough (fixing CI rides github
27
+ // or gitlab). Omitted ⇒ nothing to connect, always offered.
28
+ requires: z
29
+ .array(z.string().min(1))
30
+ .optional()
31
+ .describe(
32
+ "Capability providers that make this template work — any one connected is enough (fixing CI rides github or gitlab). Omitted ⇒ nothing to connect, so it is always offered.",
33
+ ),
34
+ // Shaped loosely here and parsed strictly at the merge: the manifest package cannot see the trigger union
35
+ // (the dependency runs the other way), so the daemon is where a declaration meets the real schema.
36
+ trigger: z
37
+ .object({
38
+ kind: z.enum(["schedule", "event", "listener", "workspace"]),
39
+ cron: z.string().min(1).optional(),
40
+ provider: z.string().min(1).optional(),
41
+ eventType: z.string().min(1).optional(),
42
+ event: z.string().min(1).optional(),
43
+ })
44
+ .describe(
45
+ "What wakes it. Checked against the real trigger schema when the daemon builds the catalogue, so a template can never offer one that would be refused.",
46
+ ),
47
+ guard: z
48
+ .string()
49
+ .min(1)
50
+ .optional()
51
+ .describe("A condition that must hold before the turn runs — what makes a template safe to leave switched on."),
52
+ holdForSeconds: z.number().int().positive().optional().describe("Wait this long and coalesce repeats, rather than firing on every event."),
53
+ prompt: z.string().min(1).describe("The turn this starts. You own the trigger's payload vocabulary, so you own the prompt that reads it."),
54
+ note: z.string().min(1).optional(),
55
+ setup: z.string().min(1).optional().describe("What the user must do themselves before this can work."),
56
+ description: z.string().min(1).optional(),
57
+ /* Absent ⇒ the create dialog's gallery, where you go once you know what you want. `create` puts a card on
58
+ * the page that makes the automation switched off in one click; `configure` puts one there that opens the
59
+ * dialog prefilled, for a template that cannot work unconfigured. Both are for what a user would never
60
+ * think to go looking for, and a pack that marked everything as offered would have built a gallery with
61
+ * extra steps. */
62
+ offer: z
63
+ .enum(["create", "configure"])
64
+ .optional()
65
+ .describe(
66
+ "Absent ⇒ it waits in the gallery, where you go once you know what you want. `create` puts a card on the page that makes it, switched off, in one click. `configure` puts one there that opens the dialog prefilled, for a template that cannot work unconfigured. Both are for what a user would never think to go looking for — mark everything as offered and you have rebuilt the gallery with extra steps.",
67
+ ),
68
+ // Whether what this makes watches THIS codebase (the chores shelf) rather than the outside world. Declared
69
+ // rather than read off the trigger: a nightly dependency sweep and a nightly Stripe poll are both schedules.
70
+ chore: z
71
+ .boolean()
72
+ .optional()
73
+ .describe(
74
+ "Whether what this makes watches THIS codebase rather than the outside world. Declared rather than read off the trigger — a nightly dependency sweep and a nightly Stripe poll are both schedules.",
75
+ ),
76
+ });
77
+ export type AutomationTemplateContribution = z.infer<typeof AutomationTemplateContributionSchema>;
78
+
79
+ export const automationTemplatesPoint = {
80
+ name: "automationTemplates",
81
+ description:
82
+ "Starting points this pack offers in the automation composer — a trigger, a prompt written for that trigger's payload, and whatever guard makes it safe to leave on. Declared by whoever knows the service rather than by the composer, so they appear when your pack is installed and disappear with it. Pure prefill: creating one makes an ordinary automation.",
83
+ schema: z.array(AutomationTemplateContributionSchema),
84
+ } as const satisfies ContributionPoint;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import type { ContributionPoint } from "../contribution-point.js";
3
+
4
+ // A checkout-relative directory of executables the daemon prepends to the AGENT's PATH each turn — how an
5
+ // extension ships a command-line tool for the agent (the CLI-tools path). The files ARE the approved code (they
6
+ // ride the sha-pinned checkout); the daemon only adds the dir to PATH.
7
+ export const binPoint = {
8
+ name: "bin",
9
+ description:
10
+ "A checkout-relative directory of executables the daemon puts on the agent's PATH every turn — how you ship the agent a command-line tool. The files are the approved code themselves: they ride the pinned checkout, and the daemon only adds the directory to PATH.",
11
+ schema: z
12
+ .string()
13
+ .min(1)
14
+ .refine((value) => !value.split("/").includes(".."), { message: "bin must stay inside the checkout" }),
15
+ } as const satisfies ContributionPoint;