@intentic/extension-manifest 1.223.0 → 1.224.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/package.json +2 -2
- package/src/bundle.ts +3 -3
- package/src/contribution-point.ts +5 -5
- package/src/json-schema.ts +6 -6
- package/src/manifest.ts +13 -13
- package/src/mark.ts +8 -8
- package/src/permissions.ts +1 -1
- package/src/points/agent.ts +1 -1
- package/src/points/automation-templates.ts +4 -4
- package/src/points/bin.ts +1 -1
- package/src/points/capabilities.ts +25 -25
- package/src/points/commands.ts +3 -3
- package/src/points/documents.ts +2 -2
- package/src/points/files.ts +7 -7
- package/src/points/index.ts +4 -4
- package/src/points/listener.ts +4 -4
- package/src/points/viewers.ts +5 -5
- package/src/powers-diff.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/extension-manifest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.224.0",
|
|
4
4
|
"description": "What an intentic extension DECLARES — the intentic-extension.json schema and the sandbox-route allowlist rule",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"tslib": "2.8.1",
|
|
36
36
|
"zod": "4.4.3",
|
|
37
|
-
"@intentic/base": "1.
|
|
37
|
+
"@intentic/base": "1.224.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "24.13.2",
|
package/src/bundle.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/* WHAT A PUBLISHED BUNDLE MAY IMPORT
|
|
1
|
+
/* WHAT A PUBLISHED BUNDLE MAY IMPORT, the loader's side of the manifest contract, stated where both sides can
|
|
2
2
|
* read it. The host fetches an extension's entry bytes and imports them from a blob: URL, which has two hard
|
|
3
3
|
* consequences: a relative import resolves against a blob: URL that was never created (a 404 for a file that
|
|
4
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
|
|
5
|
+
* invisible to the author, their own workspace loads the directory live, and fatal for every installer.
|
|
6
6
|
*
|
|
7
7
|
* It lives HERE, beside the manifest schema, because two independent judges have to agree on it: the daemon's
|
|
8
8
|
* readiness check (before an author publishes) and the registry scanner (re-deriving the same answer cold, at
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export const HOST_PUBLISHED_SPECIFIERS = ["vue", "@intentic/extension-api", "@intentic/extension-ui", "@tanstack/vue-query"] as const;
|
|
15
15
|
|
|
16
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
|
|
17
|
+
// dynamic import(). A bundle is one file by contract, so a regex over its text is the right instrument, there
|
|
18
18
|
// is no module graph to walk.
|
|
19
19
|
export const bundleSpecifiers = (source: string): string[] => [
|
|
20
20
|
...new Set([
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
|
|
3
|
-
/* A CONTRIBUTION POINT, DEFINED ONCE
|
|
3
|
+
/* A CONTRIBUTION POINT, DEFINED ONCE, its key, its shape, and the sentence that explains it to the person
|
|
4
4
|
* writing the manifest, in one object.
|
|
5
5
|
*
|
|
6
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
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
|
|
9
|
-
* for
|
|
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
10
|
* with a misspelt contribution point dropped in silence rather than named.
|
|
11
11
|
*
|
|
12
12
|
* Binding the description to the schema is what changes that: it rides `z.describe`, so it reaches the generated
|
|
13
13
|
* authoring schema (json-schema.ts) and comes back as hover text in the author's editor. The prose that is for
|
|
14
|
-
* MAINTAINERS
|
|
14
|
+
* MAINTAINERS, why a point exists, what it replaced, what it deliberately does not do, stays a comment in the
|
|
15
15
|
* point's own file, because it is not what someone filling in a field needs to read. */
|
|
16
16
|
export interface ContributionPoint<Name extends string = string, Schema extends z.ZodType = z.ZodType> {
|
|
17
17
|
// The key under `contributes` in intentic-extension.json.
|
|
@@ -20,7 +20,7 @@ export interface ContributionPoint<Name extends string = string, Schema extends
|
|
|
20
20
|
* them, and what the host does with it. It lands verbatim in editor hover text, so a paragraph of rationale
|
|
21
21
|
* here is a paragraph in a tooltip. */
|
|
22
22
|
readonly description: string;
|
|
23
|
-
// The value shape under that key
|
|
23
|
+
// The value shape under that key, an array for a point that takes many entries, the entry itself for a
|
|
24
24
|
// point that takes one. `contributes` makes every one of them optional; a point is never required.
|
|
25
25
|
readonly schema: Schema;
|
|
26
26
|
}
|
package/src/json-schema.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ExtensionManifestSchema } from "./manifest.js";
|
|
3
3
|
|
|
4
|
-
/* THE AUTHORING SCHEMA
|
|
4
|
+
/* THE AUTHORING SCHEMA, what an editor reads to help someone write `intentic-extension.json`.
|
|
5
5
|
*
|
|
6
6
|
* A manifest was previously written by copying another extension's and guessing. Nothing told the author what a
|
|
7
7
|
* field meant, because every explanation lived in a `//` comment in this package; and nothing told them when
|
|
8
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
|
|
9
|
+
* `viewers` was not an error, it was a viewer that never appeared, discovered at install, with the manifest
|
|
10
10
|
* parsing perfectly.
|
|
11
11
|
*
|
|
12
12
|
* So: the same points, emitted as JSON Schema, with the descriptions that ride each one (contribution-point.ts)
|
|
@@ -15,7 +15,7 @@ import { ExtensionManifestSchema } from "./manifest.js";
|
|
|
15
15
|
*
|
|
16
16
|
* STRICT HERE, LENIENT AT RUNTIME, and the asymmetry is the point. Authoring is where an unknown key is a typo
|
|
17
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
|
|
18
|
+
* older daemon must go on installing with the point it doesn't understand ignored, refusing it outright would
|
|
19
19
|
* make every addition to this list a breaking change. */
|
|
20
20
|
|
|
21
21
|
// Where the published copy answers, so `$schema` in a manifest resolves for an author who has installed nothing.
|
|
@@ -23,7 +23,7 @@ export const MANIFEST_SCHEMA_URL = "https://intentic.dev/intentic-extension.sche
|
|
|
23
23
|
|
|
24
24
|
/* `additionalProperties: false` on every object node, so a key nothing declares is flagged where it is typed.
|
|
25
25
|
*
|
|
26
|
-
* Skips a node that already carries `additionalProperties
|
|
26
|
+
* Skips a node that already carries `additionalProperties`, that is a `z.record`, whose whole shape is "any key,
|
|
27
27
|
* this value" (a cli capability's `env`), and pinning it closed would reject every entry it exists to accept. */
|
|
28
28
|
const closeToUnknownKeys = (node: unknown): void => {
|
|
29
29
|
if (Array.isArray(node)) {
|
|
@@ -44,7 +44,7 @@ const closeToUnknownKeys = (node: unknown): void => {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
/* The manifest schema as JSON Schema. `io: "input"` because this describes what an author WRITES
|
|
47
|
+
/* The manifest schema as JSON Schema. `io: "input"` because this describes what an author WRITES, the shape
|
|
48
48
|
* going in, before any refinement or default has been applied to it. */
|
|
49
49
|
export const manifestJsonSchema = (): Record<string, unknown> => {
|
|
50
50
|
const schema = z.toJSONSchema(ExtensionManifestSchema, { unrepresentable: "any", io: "input" }) as Record<string, unknown>;
|
|
@@ -58,5 +58,5 @@ export const manifestJsonSchema = (): Record<string, unknown> => {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
// The committed file's exact bytes, so the generator and the check that guards it cannot disagree about
|
|
61
|
-
// formatting
|
|
61
|
+
// formatting, four spaces and a trailing newline, the repo's shape for a committed generated document.
|
|
62
62
|
export const serializeManifestJsonSchema = (schema: Record<string, unknown>): string => `${JSON.stringify(schema, undefined, 4)}\n`;
|
package/src/manifest.ts
CHANGED
|
@@ -3,25 +3,25 @@ import { MARK_FIELDS } from "./mark.js";
|
|
|
3
3
|
import { contributesSchema } from "./points/index.js";
|
|
4
4
|
|
|
5
5
|
/* The extension manifest: `intentic-extension.json` at the extension repo root (deliberately NOT inside
|
|
6
|
-
* .claude-plugin
|
|
6
|
+
* .claude-plugin/, that directory is Claude Code's namespace with its own semantics). The manifest is the
|
|
7
7
|
* approval surface: the install dialog shows exactly these declared contributions before the owner confirms,
|
|
8
8
|
* and the host refuses runtime registrations (views, commands) whose ids the approved manifest never declared.
|
|
9
9
|
*
|
|
10
|
-
* This file is the ENVELOPE only
|
|
10
|
+
* This file is the ENVELOPE only, who the extension is, which host it needs, what code it ships, how far it
|
|
11
11
|
* may reach. What it may CONTRIBUTE is one file per contribution point under points/, assembled here; see
|
|
12
12
|
* contribution-point.ts for why the description travels with the schema instead of sitting in a comment. */
|
|
13
13
|
|
|
14
14
|
export const ExtensionManifestSchema = z.object({
|
|
15
|
-
/* The authoring schema this manifest is written against
|
|
15
|
+
/* The authoring schema this manifest is written against, editors read it and give the author completion,
|
|
16
16
|
* hover text and a red squiggle on a misspelt key. Declared so it survives the parse rather than being
|
|
17
17
|
* silently stripped, which is what would otherwise happen to the one field an author is most likely to add
|
|
18
18
|
* by hand. Nothing at runtime reads it. */
|
|
19
19
|
$schema: z.string().optional().describe("The authoring schema, for editor completion and validation. Nothing at runtime reads it."),
|
|
20
20
|
publisher: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
21
21
|
name: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
22
|
-
// The extension's own semver
|
|
22
|
+
// The extension's own semver, display/identity only; the installed code identity is the pinned commit sha.
|
|
23
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
|
|
24
|
+
/* The section this extension sits under in the Sandbox hub's Extensions tab, a grouping by what it is FOR,
|
|
25
25
|
* declared because it cannot be derived. Nine of the first-party extensions contribute a rail tile, so a
|
|
26
26
|
* grouping read off `contributes` puts more than half the list in one section and says nothing about any of
|
|
27
27
|
* them. Deliberately a loose string, exactly like a connector's `catalog.category`: the vocabulary belongs
|
|
@@ -34,13 +34,13 @@ export const ExtensionManifestSchema = z.object({
|
|
|
34
34
|
.describe(
|
|
35
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
36
|
),
|
|
37
|
-
/* What the extension is drawn as wherever it is LISTED rather than used
|
|
37
|
+
/* What the extension is drawn as wherever it is LISTED rather than used, the Extensions tab, a registry
|
|
38
38
|
* being browsed, the gallery. Deliberately here and not on a view: `Activation.icon` is the glyph of one
|
|
39
39
|
* rail tile, it only exists once the extension's code has activated in this browser, and nine of the
|
|
40
40
|
* first-party extensions register no view at all. An extension that is switched off, daemon-only, or not
|
|
41
41
|
* yet installed still has to look like something. See MARK_FIELDS. */
|
|
42
42
|
...MARK_FIELDS,
|
|
43
|
-
// Semver range over the host's extension API version (extensionApiVersion)
|
|
43
|
+
// Semver range over the host's extension API version (extensionApiVersion), checked before activation.
|
|
44
44
|
engines: z
|
|
45
45
|
.object({ intentic: z.string().min(1) })
|
|
46
46
|
.describe("A semver range over the host's extension API version, checked before your code is activated."),
|
|
@@ -54,7 +54,7 @@ export const ExtensionManifestSchema = z.object({
|
|
|
54
54
|
.describe(
|
|
55
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
56
|
),
|
|
57
|
-
/* Repo-relative path of the prebuilt single-file node ESM SERVER bundle
|
|
57
|
+
/* Repo-relative path of the prebuilt single-file node ESM SERVER bundle, the extension's BACKEND half,
|
|
58
58
|
* exporting `activateServer(api, context)`. Loaded by the daemon's backend host (a separate supervised
|
|
59
59
|
* process, so a toggle or a live edit is a host restart rather than a daemon death) and served under the
|
|
60
60
|
* extension's own route namespace `/x/<id>/…`, which the daemon proxies. Self-contained by construction:
|
|
@@ -68,12 +68,12 @@ export const ExtensionManifestSchema = z.object({
|
|
|
68
68
|
.describe(
|
|
69
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
70
|
),
|
|
71
|
-
// Declared reach, both halves in one grammar
|
|
72
|
-
// (e.g. "GET /panels", "POST /panels/*/start")
|
|
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
73
|
// one vocabulary.
|
|
74
|
-
// sandbox
|
|
74
|
+
// sandbox, the daemon routes the UI half may call through api.sandbox (the host refuses undeclared
|
|
75
75
|
// ones). An extension's OWN namespace `/x/<its id>/…` needs no entry: its backend is its own.
|
|
76
|
-
// daemon
|
|
76
|
+
// daemon , the daemon routes the SERVER half may call through api.daemon, enforced by the daemon's
|
|
77
77
|
// extension-token grant. Separate from `sandbox` because the halves run as different
|
|
78
78
|
// principals: the UI acts with the owner's session, the backend with a minted per-extension
|
|
79
79
|
// token, and a grant to one must never quietly widen the other.
|
|
@@ -99,6 +99,6 @@ export const ExtensionManifestSchema = z.object({
|
|
|
99
99
|
});
|
|
100
100
|
export type ExtensionManifest = z.infer<typeof ExtensionManifestSchema>;
|
|
101
101
|
|
|
102
|
-
// The extension's identity everywhere (capability entries, /ext routes, settings namespaces)
|
|
102
|
+
// The extension's identity everywhere (capability entries, /ext routes, settings namespaces), derived, never
|
|
103
103
|
// declared, so it can't contradict the publisher/name the install dialog showed.
|
|
104
104
|
export const extensionIdOf = (manifest: Pick<ExtensionManifest, "publisher" | "name">): string => `${manifest.publisher}.${manifest.name}`;
|
package/src/mark.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
/* HOW SOMETHING LOOKS BEFORE ANY OF ITS CODE RUNS
|
|
3
|
+
/* HOW SOMETHING LOOKS BEFORE ANY OF ITS CODE RUNS, the mark a capability card and an extension are drawn
|
|
4
4
|
* with, in ONE shape because one component draws both (<BrandMark>) and a second copy of these two fields is a
|
|
5
5
|
* second answer to what happens when a slug 404s.
|
|
6
6
|
*
|
|
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
* author controls completely, and the only one that can make a grid of unfamiliar names look like a shelf of
|
|
9
9
|
* distinct products rather than a column of identical glyphs. `logo` is a simple-icons slug fetched from a
|
|
10
10
|
* CDN: exactly right for a card standing in for somebody else's product (GitHub, Postgres, Slack), useless
|
|
11
|
-
* for the many things that have no brand in that set, and unreachable in an offline sandbox
|
|
11
|
+
* for the many things that have no brand in that set, and unreachable in an offline sandbox, so it can never
|
|
12
12
|
* be the only tier. `icon` is a name from the host's own bundled vocabulary, which ships in the image, follows
|
|
13
13
|
* the theme and costs no request; it is what carries a first-party extension that has not been drawn yet. What
|
|
14
14
|
* declares none of them is drawn as its initials, so no row is ever blank and no author is obliged to have a
|
|
@@ -16,15 +16,15 @@ import { z } from "zod";
|
|
|
16
16
|
*
|
|
17
17
|
* ART BEATS A BRAND SLUG because they answer different questions. A slug says "this is Slack"; art says "this
|
|
18
18
|
* is mine". An extension that stands in for somebody else's product should declare the slug and no art, and
|
|
19
|
-
* one that is its own thing should declare art
|
|
19
|
+
* one that is its own thing should declare art, but where both arrive, the author's own drawing is the more
|
|
20
20
|
* specific claim and wins.
|
|
21
21
|
*
|
|
22
22
|
* Artwork that will not parse, a slug that fails to load and an icon name this build has never heard of all
|
|
23
|
-
* fall to the tier BELOW rather than to a hole
|
|
23
|
+
* fall to the tier BELOW rather than to a hole, the rule the rail already applies to Activation.icon, here for
|
|
24
24
|
* the surfaces that must draw an extension whose code is not running: one that is switched off, one that is
|
|
25
25
|
* daemon-only, one being read about in a registry before it is installed at all. */
|
|
26
26
|
|
|
27
|
-
/* Big enough for a drawn mark, far too small for a traced photograph
|
|
27
|
+
/* Big enough for a drawn mark, far too small for a traced photograph, which is the line being drawn. This
|
|
28
28
|
* string rides every registry row and every manifest read, so it is a budget as much as a limit: at 4 KB the
|
|
29
29
|
* whole official registry's artwork costs less than one screenshot, and an author who needs more than that is
|
|
30
30
|
* shipping a raster they should be shipping as a brand slug or not at all. */
|
|
@@ -33,18 +33,18 @@ const ART_MAX_BYTES = 4096;
|
|
|
33
33
|
export const MARK_FIELDS = {
|
|
34
34
|
/* THE SVG DOCUMENT ITSELF, not a URL and not base64.
|
|
35
35
|
*
|
|
36
|
-
* A URL would put a stranger's server in the render path of a page listing extensions
|
|
36
|
+
* A URL would put a stranger's server in the render path of a page listing extensions, a fetch that
|
|
37
37
|
* tracks who is browsing what, breaks in an offline sandbox, and 404s long after the row was approved:
|
|
38
38
|
* the three failures the `logo` tier already documents, with none of its excuse. Inline costs one string
|
|
39
39
|
* and always draws.
|
|
40
40
|
*
|
|
41
41
|
* Kept as READABLE TEXT rather than a data URI because of who reads it. The registry's curated file is
|
|
42
42
|
* reviewed by a human before anything is published, and `<rect fill="#5B4FE9"/><circle .../>` can be read
|
|
43
|
-
* in a diff, while base64 is a wall nobody checks
|
|
43
|
+
* in a diff, while base64 is a wall nobody checks, an opaque blob in the one file whose entire purpose is
|
|
44
44
|
* to be checked. The renderer does its own encoding at the point of use.
|
|
45
45
|
*
|
|
46
46
|
* It is drawn INERT (an <img>, never inline in the document), so a hostile registry row cannot script the
|
|
47
|
-
* page it is listed on
|
|
47
|
+
* page it is listed on, see <BrandMark>, which owns that guarantee and the sniff test that enforces it. */
|
|
48
48
|
art: z
|
|
49
49
|
.string()
|
|
50
50
|
.max(ART_MAX_BYTES)
|
package/src/permissions.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* The sandbox-route permission model. An extension declares in its manifest exactly which daemon routes it may
|
|
2
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
|
|
3
|
+
* segment. The host matches every call against these and refuses an undeclared route, so an extension's backend
|
|
4
4
|
* reach is explicit, diffable, and reviewable rather than an ambient client to the whole daemon. */
|
|
5
5
|
|
|
6
6
|
const escapeRegExp = (literal: string): string => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
package/src/points/agent.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
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
|
|
5
|
+
// loader, which reads skills/agents/hooks/commands/.mcp.json each turn, the daemon never parses plugin
|
|
6
6
|
// internals.
|
|
7
7
|
export const AgentContributionSchema = z.object({
|
|
8
8
|
path: z.string().optional().describe("Relative to the extension checkout. Absent ⇒ the checkout root."),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
|
-
/* A STARTING POINT in the automation composer
|
|
4
|
+
/* A STARTING POINT in the automation composer, a trigger, a prompt written for that trigger's payload, and
|
|
5
5
|
* whatever guard or hold makes it safe to leave on. Pure prefill: creating one makes an ordinary automation and
|
|
6
6
|
* the daemon knows nothing about templates afterwards.
|
|
7
7
|
*
|
|
8
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
|
|
9
|
+
* automation surface used to carry every one of these. Komodo's, Sentry's, Stripe's, CI's, the chore book's,
|
|
10
10
|
* so a pack that gained something worth reacting to could not say so without an edit to a surface it has
|
|
11
11
|
* nothing to do with. A template declared here appears when the pack is installed and its capability connected,
|
|
12
12
|
* and disappears with it.
|
|
@@ -14,7 +14,7 @@ import type { ContributionPoint } from "../contribution-point.js";
|
|
|
14
14
|
* The daemon validates each one against the real trigger schema when it builds the catalogue and drops what
|
|
15
15
|
* does not parse, so a template can never offer a trigger that `upsert` would refuse. */
|
|
16
16
|
export const AutomationTemplateContributionSchema = z.object({
|
|
17
|
-
// Prefills the automation name, and is what "does one of these exist already" is asked by
|
|
17
|
+
// Prefills the automation name, and is what "does one of these exist already" is asked by, so it must be
|
|
18
18
|
// spelled as an automation id, not as prose.
|
|
19
19
|
id: z
|
|
20
20
|
.string()
|
|
@@ -23,7 +23,7 @@ export const AutomationTemplateContributionSchema = z.object({
|
|
|
23
23
|
title: z.string().min(1),
|
|
24
24
|
logo: z.string().min(1).optional().describe("A simple-icons slug for the card."),
|
|
25
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
|
|
26
|
+
// Capability providers that make this template WORK, any one connected is enough (fixing CI rides github
|
|
27
27
|
// or gitlab). Omitted ⇒ nothing to connect, always offered.
|
|
28
28
|
requires: z
|
|
29
29
|
.array(z.string().min(1))
|
package/src/points/bin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
|
-
// A checkout-relative directory of executables the daemon prepends to the AGENT's PATH each turn
|
|
4
|
+
// A checkout-relative directory of executables the daemon prepends to the AGENT's PATH each turn, how an
|
|
5
5
|
// extension ships a command-line tool for the agent (the CLI-tools path). The files ARE the approved code (they
|
|
6
6
|
// ride the sha-pinned checkout); the daemon only adds the dir to PATH.
|
|
7
7
|
export const binPoint = {
|
|
@@ -13,7 +13,7 @@ export const CapabilityFieldSchema = z.object({
|
|
|
13
13
|
secret: z.boolean().optional().describe("Mask it, and never echo it back."),
|
|
14
14
|
optional: z.boolean().optional(),
|
|
15
15
|
multiline: z.boolean().optional(),
|
|
16
|
-
// An OPT-IN EXTRA rather than a decision
|
|
16
|
+
// An OPT-IN EXTRA rather than a decision, rendered as a switch, carried as the "on"/"off" the config
|
|
17
17
|
// schemas already speak (the vpn's pfs/aggressive precedent). A two-option Segmented can express the same
|
|
18
18
|
// value, and reads wrong for this: it presents a choice the user must make to proceed, sized like the
|
|
19
19
|
// required fields around it. Something the capability works fine without wants the control that is quiet
|
|
@@ -34,7 +34,7 @@ export const CapabilityFieldSchema = z.object({
|
|
|
34
34
|
.describe(
|
|
35
35
|
"A line under this control, for what the label alone cannot say — a host requirement, when a value takes effect. The card's own `hint` speaks for the whole card; this one is bound to the field it qualifies.",
|
|
36
36
|
),
|
|
37
|
-
/* This field's value only takes effect after the sandbox is REBUILT
|
|
37
|
+
/* This field's value only takes effect after the sandbox is REBUILT, it rides the environment overlay
|
|
38
38
|
* rather than something the daemon can act on now. Rendered as a chip beside the label.
|
|
39
39
|
*
|
|
40
40
|
* The one fact a user needs before touching a control, and the one the form cannot infer: two switches
|
|
@@ -52,12 +52,12 @@ export const CapabilityFieldSchema = z.object({
|
|
|
52
52
|
.array(z.object({ value: z.string(), label: z.string() }))
|
|
53
53
|
.optional()
|
|
54
54
|
.describe("Turns the field into a select."),
|
|
55
|
-
/* Gates this field on the answers already given
|
|
55
|
+
/* Gates this field on the answers already given, the SSH credential that only applies to the auth mode
|
|
56
56
|
* chosen, the gateway fields that belong to one VPN provider. A `when` condition (@intentic/base/when)
|
|
57
57
|
* evaluated against the form's live values, so it re-reads as the user toggles.
|
|
58
58
|
*
|
|
59
59
|
* Refused at parse when it does not parse. A card is data an extension ships, and a condition nobody can
|
|
60
|
-
* evaluate is not a field that is always shown or always hidden
|
|
60
|
+
* evaluate is not a field that is always shown or always hidden, it is a card whose author believes it
|
|
61
61
|
* asks something it never asks. Failing the manifest names the card; failing at render names nothing. */
|
|
62
62
|
when: z
|
|
63
63
|
.string()
|
|
@@ -66,7 +66,7 @@ export const CapabilityFieldSchema = z.object({
|
|
|
66
66
|
.describe(
|
|
67
67
|
"Only show this field while a condition over the answers already given holds — `auth == 'key'`, `provider in ['ipsec', 'fortinet']`, `!advanced`. Supports `&&`, `||`, `!`, comparisons and `in`.",
|
|
68
68
|
),
|
|
69
|
-
// A fixed value baked into the config rather than asked for
|
|
69
|
+
// A fixed value baked into the config rather than asked for, how a card pins a discriminator
|
|
70
70
|
// (platform="reddit", provider="stripe"). Rendered as nothing; sent as itself.
|
|
71
71
|
value: z
|
|
72
72
|
.string()
|
|
@@ -74,9 +74,9 @@ export const CapabilityFieldSchema = z.object({
|
|
|
74
74
|
.describe(
|
|
75
75
|
'A fixed value baked into the config rather than asked for — how a card pins its discriminator (platform="reddit", provider="stripe"). Renders as nothing.',
|
|
76
76
|
),
|
|
77
|
-
/* This field holds a TOTP seed
|
|
77
|
+
/* This field holds a TOTP seed, the base32 key (or otpauth:// URI) a service shows when enrolling an
|
|
78
78
|
* authenticator app. Declare it WITH `secret: true`: the seed is a durable second factor, so it is never
|
|
79
|
-
* echoed and, unlike an ordinary secret, never enters the agent's environment either
|
|
79
|
+
* echoed and, unlike an ordinary secret, never enters the agent's environment either, the daemon mints the
|
|
80
80
|
* six-digit codes on demand (`otp <name>` / GET /capabilities/<id>/otp) and only those cross, each dead
|
|
81
81
|
* within its period. A cli entry whose env references a totp field therefore fails to parse (see below). */
|
|
82
82
|
totp: z
|
|
@@ -88,7 +88,7 @@ export const CapabilityFieldSchema = z.object({
|
|
|
88
88
|
});
|
|
89
89
|
export type CapabilityField = z.infer<typeof CapabilityFieldSchema>;
|
|
90
90
|
|
|
91
|
-
/* WHETHER A FIELD IS IN PLAY, given what has been answered so far
|
|
91
|
+
/* WHETHER A FIELD IS IN PLAY, given what has been answered so far, and the only place that decides it.
|
|
92
92
|
*
|
|
93
93
|
* Two tiers ask this question about the same card. The web's form asks it to decide what to draw and what to
|
|
94
94
|
* validate; the daemon asks it at install to decide which fields it may demand. They used to answer it with
|
|
@@ -98,7 +98,7 @@ export type CapabilityField = z.infer<typeof CapabilityFieldSchema>;
|
|
|
98
98
|
* report about one card rather than as a broken rule.
|
|
99
99
|
*
|
|
100
100
|
* It lives beside the schema for the reason the description does: this is a fact about the shape, and the two
|
|
101
|
-
* consumers are in different packages. Parsed per call rather than cached
|
|
101
|
+
* consumers are in different packages. Parsed per call rather than cached, a card has a handful of fields,
|
|
102
102
|
* this runs on a keystroke at worst, and a cache keyed by manifest strings is a map that outlives every
|
|
103
103
|
* extension that ever declared one. */
|
|
104
104
|
export const fieldApplies = (field: CapabilityField, values: Readonly<Record<string, unknown>>): boolean =>
|
|
@@ -109,7 +109,7 @@ export const fieldApplies = (field: CapabilityField, values: Readonly<Record<str
|
|
|
109
109
|
const CatalogSchema = z.object({
|
|
110
110
|
name: z.string().min(1),
|
|
111
111
|
...MARK_FIELDS,
|
|
112
|
-
// ONE LINE
|
|
112
|
+
// ONE LINE, aim for 60 characters or fewer. The grid clamps this at two lines and a card sits beside two
|
|
113
113
|
// others in a pane the index column has already taken 16rem out of, so a paragraph here is a paragraph the
|
|
114
114
|
// reader gets truncated. Everything longer belongs in `hint`, which the config form prints in full and the
|
|
115
115
|
// catalog's search reads. Not capped in the schema: an extension published before this rule should still
|
|
@@ -121,7 +121,7 @@ const CatalogSchema = z.object({
|
|
|
121
121
|
"ONE LINE — aim for 60 characters or fewer. The grid clamps it at two lines in a narrow pane, so a paragraph here is a paragraph the reader gets truncated. Everything longer belongs in `hint`.",
|
|
122
122
|
),
|
|
123
123
|
category: z.string().min(1),
|
|
124
|
-
// The paragraph. Shown under the add-form, and searched from the catalog
|
|
124
|
+
// The paragraph. Shown under the add-form, and searched from the catalog, so the words that identify this
|
|
125
125
|
// card to someone hunting for it ("webauthn", "socket mode") belong here even when the tile can't show them.
|
|
126
126
|
hint: z
|
|
127
127
|
.string()
|
|
@@ -151,24 +151,24 @@ const contributionBase = {
|
|
|
151
151
|
fields: z.array(CapabilityFieldSchema),
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
-
/* A CAPABILITY CARD AS DATA. The catalog is the extensible layer; the HANDLERS are core
|
|
154
|
+
/* A CAPABILITY CARD AS DATA. The catalog is the extensible layer; the HANDLERS are core, so an entry names one
|
|
155
155
|
* of the kinds whose daemon-side machinery is fully generic over its data, and the machinery stays put. The
|
|
156
156
|
* kinds NOT listed here are the ones whose card is one-to-one with a handler that owns real privilege (`docker`
|
|
157
157
|
* bakes --privileged, `vpn` bakes NET_ADMIN, `extension` installs extensions, `devops` scaffolds repos): their
|
|
158
158
|
* cards live in the platform catalog because separating card from handler would split one concept in two, and
|
|
159
159
|
* because a manifest that could name them would be a manifest that grants itself privilege. That restriction is
|
|
160
|
-
* this discriminated union, not a comment
|
|
160
|
+
* this discriminated union, not a comment, a manifest naming any other kind fails to parse.
|
|
161
161
|
*
|
|
162
162
|
* `${id}` in a cli/host skill file is substituted with the instance name at apply time (so a host pack's tool
|
|
163
163
|
* names read `mcp__my-laptop__run_command`), and, for `cli`, each `$ENVVAR` becomes its per-instance suffixed
|
|
164
|
-
* name. A BROWSER pack's skill renders once per SITE rather than per instance
|
|
164
|
+
* name. A BROWSER pack's skill renders once per SITE rather than per instance, its seams are `${accounts}`
|
|
165
165
|
* (the roster of connected accounts), `${tools}` (the core driving/connecting note) and `${site}` (the host,
|
|
166
166
|
* for the generic card whose text can name no site of its own); `${id}` and per-field substitution do not
|
|
167
167
|
* apply there (capabilities/account-skills.ts in the sandbox daemon). */
|
|
168
168
|
export const CapabilityContributionSchema = z
|
|
169
169
|
.discriminatedUnion("kind", [
|
|
170
170
|
// A CLI tool the AGENT gets, authenticated: the env vars its shell receives (value templates over the
|
|
171
|
-
// fields
|
|
171
|
+
// fields, `${field}` substitutes, `${field:uri}` percent-encodes), a SKILL.md cheatsheet, and an optional
|
|
172
172
|
// image fragment holding the client binary (psql, mysql, whisper).
|
|
173
173
|
z.object({
|
|
174
174
|
...contributionBase,
|
|
@@ -191,18 +191,18 @@ export const CapabilityContributionSchema = z
|
|
|
191
191
|
}),
|
|
192
192
|
/* A site the agent acts on AS THE OWNER, through the shared logged-in Chromium. `loginUrl` is what the
|
|
193
193
|
* sign-in window opens; the profile it persists is the credential. `homeUrl` is where that same profile
|
|
194
|
-
* opens once it HAS one
|
|
194
|
+
* opens once it HAS one, the owner's own hands on the connected browser, which a login page is the wrong
|
|
195
195
|
* place to start (signed in, it only redirects). Two fields because for some platforms the login lives on
|
|
196
196
|
* another site entirely (YouTube signs in at accounts.google.com), so one cannot be derived from the other.
|
|
197
197
|
* No `env` and no `fragment`: the browser itself is core (one Chromium install serves every platform),
|
|
198
|
-
* only the identity is per-entry. A card never declares the account's username/password either
|
|
198
|
+
* only the identity is per-entry. A card never declares the account's username/password either, those are
|
|
199
199
|
* CORE form fields on every browser card (the catalog appends them; the daemon's add validation accepts
|
|
200
200
|
* them), because which box a login form wants filled is the same fact on every site.
|
|
201
201
|
*
|
|
202
202
|
* BOTH URLs ARE OPTIONAL, so that one card can be the GENERIC one: a site card pins them (Reddit knows
|
|
203
203
|
* where Reddit signs in), and the generic "browser session" card asks for them on its form instead, which
|
|
204
|
-
* is what lets a user connect a site nobody shipped a card for. A card must do one or the other
|
|
205
|
-
* URL or declare a field that supplies it
|
|
204
|
+
* is what lets a user connect a site nobody shipped a card for. A card must do one or the other, pin a
|
|
205
|
+
* URL or declare a field that supplies it, and the daemon's apply says so on the form when neither does,
|
|
206
206
|
* because the alternative is a sign-in window that opens on nothing. */
|
|
207
207
|
z.object({
|
|
208
208
|
...contributionBase,
|
|
@@ -226,7 +226,7 @@ export const CapabilityContributionSchema = z
|
|
|
226
226
|
"Checkout-relative SKILL.md teaching the agent this site's actions — rendered once per site, all its connected accounts on one roster (`${accounts}`), the core tool note at `${tools}`.",
|
|
227
227
|
),
|
|
228
228
|
}),
|
|
229
|
-
// An operating system a connected computer can run
|
|
229
|
+
// An operating system a connected computer can run, the skill pack that teaches the agent THAT machine's
|
|
230
230
|
// shell. The enrollment, the socket and the scope enforcement are core; only the pack varies.
|
|
231
231
|
z.object({
|
|
232
232
|
...contributionBase,
|
|
@@ -234,13 +234,13 @@ export const CapabilityContributionSchema = z
|
|
|
234
234
|
skill: z.string().min(1).describe("Checkout-relative SKILL.md teaching the agent that machine's shell."),
|
|
235
235
|
}),
|
|
236
236
|
/* A PRESET over a core kind: no payload at all, just a named card whose `fields` carry the defaults. What an
|
|
237
|
-
* ACP agent needs is a command, so "OpenCode" is entirely a name, a logo and a filled-in form
|
|
237
|
+
* ACP agent needs is a command, so "OpenCode" is entirely a name, a logo and a filled-in form, which is
|
|
238
238
|
* exactly what a catalog row is. */
|
|
239
239
|
z.object({ ...contributionBase, kind: z.literal("agent") }),
|
|
240
240
|
])
|
|
241
241
|
.superRefine((spec, ctx) => {
|
|
242
242
|
// The totp flag's one invariant, enforced where the manifest is parsed rather than trusted to authors: a
|
|
243
|
-
// seed the daemon mints codes from must never ride the env into the agent's shell
|
|
243
|
+
// seed the daemon mints codes from must never ride the env into the agent's shell, that would hand the
|
|
244
244
|
// agent the second factor itself instead of one expiring code at a time.
|
|
245
245
|
if (spec.kind !== "cli") {
|
|
246
246
|
return;
|
|
@@ -255,15 +255,15 @@ export const CapabilityContributionSchema = z
|
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
export type CapabilityContribution = z.infer<typeof CapabilityContributionSchema>;
|
|
258
|
-
// The arms carrying a per-instance SKILL.md
|
|
258
|
+
// The arms carrying a per-instance SKILL.md, the daemon templates and installs these identically.
|
|
259
259
|
export type SkillContribution = Extract<CapabilityContribution, { skill: string }>;
|
|
260
260
|
|
|
261
261
|
/* The config key a kind's cards PIN to their own id, so a stored capability can be traced back to the card that
|
|
262
|
-
* made it
|
|
262
|
+
* made it, the daemon resolves the entry's handler data through it, and the web tells one card's instances from
|
|
263
263
|
* another's. `agent` has none on purpose: its cards are presets over one config shape, differing only in their
|
|
264
264
|
* defaults, so every agent instance belongs to every agent card equally.
|
|
265
265
|
*
|
|
266
|
-
* Here, beside the schema, because it is a fact about the contribution shape
|
|
266
|
+
* Here, beside the schema, because it is a fact about the contribution shape, the daemon, the catalog and the
|
|
267
267
|
* web all need it, and three copies of it is three chances for a card's instances to go missing. `satisfies`
|
|
268
268
|
* rather than a lookup table so a new arm above is a compile error until this answers for it. */
|
|
269
269
|
const DISCRIMINATOR = { cli: "provider", browser: "platform", host: "platform", agent: undefined } satisfies Record<
|
package/src/points/commands.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const CommandContributionSchema = z.object({
|
|
|
9
9
|
icon: z.string().optional().describe("A name from the host's icon set, drawn beside the title."),
|
|
10
10
|
// An optional global keyboard shortcut, in the host's chord notation (`Mod`/`Ctrl`/`Shift`/`Alt` + key, e.g.
|
|
11
11
|
// "Mod+Shift+K"; `Mod` = ⌘ on Apple, Ctrl elsewhere). It is DECLARED here so it rides the install dialog's
|
|
12
|
-
// approval surface
|
|
12
|
+
// approval surface, a global shortcut is consequential, so like title/icon the manifest value is authoritative
|
|
13
13
|
// and the host binds only what was approved. Whitespace-free; an unparseable chord simply never fires.
|
|
14
14
|
keybinding: z
|
|
15
15
|
.string()
|
|
@@ -18,12 +18,12 @@ export const CommandContributionSchema = z.object({
|
|
|
18
18
|
.describe(
|
|
19
19
|
'A global keyboard shortcut, e.g. "Mod+Shift+K" — `Mod` is ⌘ on Apple and Ctrl elsewhere. Declared here because a global shortcut is consequential: the owner approves it at install, and the host binds only what was approved.',
|
|
20
20
|
),
|
|
21
|
-
/* When the KEYBINDING applies
|
|
21
|
+
/* When the KEYBINDING applies, a condition over the shell's context keys (`tabSurface == 'chat'`,
|
|
22
22
|
* `!editableTarget`; see @intentic/base/when). The palette ignores it: a command is always runnable by
|
|
23
23
|
* name, and what a condition decides is whether the chord is claimed from whatever else would have had it.
|
|
24
24
|
*
|
|
25
25
|
* Declared here because it could not be declared anywhere. The shell's own commands used to gate on a
|
|
26
|
-
* JavaScript predicate, which an extension has no way to ship
|
|
26
|
+
* JavaScript predicate, which an extension has no way to ship, so every extension command took its chord
|
|
27
27
|
* globally, from every surface, including terminals where a bare key belongs to the program on the other
|
|
28
28
|
* end. A condition an extension can write is what makes a contributed shortcut safe to grant. */
|
|
29
29
|
when: z
|
package/src/points/documents.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
4
|
/* A per-directory document family the extension may register at runtime (api.documents.register): the provider
|
|
5
|
-
* marks the directory rows it can explain in the Workspace tree, and the host opens its component as a tab
|
|
5
|
+
* marks the directory rows it can explain in the Workspace tree, and the host opens its component as a tab,
|
|
6
6
|
* see DocumentProviderRegistration.
|
|
7
7
|
*
|
|
8
8
|
* Only the id and the label are declared, deliberately. The consequential part of a viewer is which FILES it
|
|
9
|
-
* takes over, and of a command its global shortcut
|
|
9
|
+
* takes over, and of a command its global shortcut, both are decided in the manifest because the owner must see
|
|
10
10
|
* them. A document provider takes nothing over: it adds an icon to rows it has something for, and every one of
|
|
11
11
|
* those rows is evidence the owner can see for themselves. So the manifest gates WHETHER the extension may mark
|
|
12
12
|
* up the tree at all, and the per-row wording stays with the provider, which is the only thing that knows what
|
package/src/points/files.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
|
-
/* WHICH WORKSPACE FILE MAKES THIS EXTENSION'S VIEW STALE
|
|
4
|
+
/* WHICH WORKSPACE FILE MAKES THIS EXTENSION'S VIEW STALE, the extension's half of the core's
|
|
5
5
|
* WORKSPACE_STATE_FILES table (@intentic/sandbox-contract), in the same two fields so the browser can union them
|
|
6
6
|
* without translating.
|
|
7
7
|
*
|
|
8
8
|
* An intentic workspace is file-first: the agent edits /work with its own file tools, out of band from every
|
|
9
9
|
* HTTP route, and the daemon's filesystem watcher is the ONLY thing that can tell a browser its view went stale.
|
|
10
|
-
* Before this contribution point existed an extension had no way into that push, so every one of them polled
|
|
10
|
+
* Before this contribution point existed an extension had no way into that push, so every one of them polled,
|
|
11
11
|
* and the core's table had to hardcode `automations`/`automation-approvals`, query keys owned by an extension,
|
|
12
12
|
* because the extension itself couldn't declare them. Declaring is now the extension's job and unioning is the
|
|
13
13
|
* host's.
|
|
14
14
|
*
|
|
15
15
|
* It rides the manifest rather than a runtime api.workspace.onDidChangeFiles for two reasons: the owner sees at
|
|
16
|
-
* install which of their files an extension reads, and there is nothing imperative left to get wrong
|
|
16
|
+
* install which of their files an extension reads, and there is nothing imperative left to get wrong, no
|
|
17
17
|
* subscribe, no unsubscribe, no listener that quietly stops firing. */
|
|
18
18
|
export const FileContributionSchema = z.object({
|
|
19
|
-
/* Workspace-root-relative, forward-slash
|
|
19
|
+
/* Workspace-root-relative, forward-slash, the space the watcher's changed paths arrive in. Matched by
|
|
20
20
|
* PREFIX, so one entry covers an exact file (`.intentic/config/automations.json`), a directory (`.intentic/config/drafts/`
|
|
21
|
-
|
|
21
|
+
*, keep the trailing slash so it cannot match a sibling file) or a name family (`.intentic/environment.`).
|
|
22
22
|
* Deliberately not a glob: prefix is the whole matching rule on both sides of this union. */
|
|
23
23
|
path: z
|
|
24
24
|
.string()
|
|
@@ -29,14 +29,14 @@ export const FileContributionSchema = z.object({
|
|
|
29
29
|
.describe(
|
|
30
30
|
"Workspace-root-relative, forward-slash, matched by prefix — so one entry covers an exact file (`.intentic/config/automations.json`), a directory (`.intentic/config/drafts/`, with the trailing slash so it cannot match a sibling file) or a name family (`.intentic/environment.`). Not a glob.",
|
|
31
31
|
),
|
|
32
|
-
/* The browser query keys those contents feed
|
|
32
|
+
/* The browser query keys those contents feed, the first element of the extension's own
|
|
33
33
|
* `api.sandbox.key(...)` keys, which is what makes them match (the sandbox id is a SUFFIX). Empty is not
|
|
34
34
|
* allowed: a path that makes nothing stale is a declaration with no effect, and saying so at install beats
|
|
35
35
|
* discovering it as a view that never refreshes.
|
|
36
36
|
*
|
|
37
37
|
* Keep the paths as narrow as the view actually needs. A broad prefix costs every connected browser a
|
|
38
38
|
* refetch per matching write, and a write-heavy path (an index, a transcript, a log) turns that into a
|
|
39
|
-
* request storm
|
|
39
|
+
* request storm, the reason the core table leaves the daemon's own machine state off the push entirely. */
|
|
40
40
|
invalidates: z
|
|
41
41
|
.array(z.string().min(1))
|
|
42
42
|
.min(1)
|
package/src/points/index.ts
CHANGED
|
@@ -30,14 +30,14 @@ export * from "./views.js";
|
|
|
30
30
|
|
|
31
31
|
/* EVERYTHING A MANIFEST MAY DECLARE. `contributes` is assembled from this list (manifest.ts), the authoring
|
|
32
32
|
* JSON Schema is generated from it (json-schema.ts), and the SDK's surface guard reads the point names back out
|
|
33
|
-
* of it
|
|
33
|
+
* of it, so the three cannot disagree about what this build supports.
|
|
34
34
|
*
|
|
35
35
|
* Collected explicitly rather than by a module-load side effect, because two readers need the answer to be the
|
|
36
36
|
* same every time it is asked: the wire contract's lock file, which is a committed document a diff has to be
|
|
37
37
|
* able to guard, and the generated schema, which is committed too. A registry that filled itself as modules
|
|
38
38
|
* happened to load would make both of those depend on import order.
|
|
39
39
|
*
|
|
40
|
-
* Adding a point is a file in this directory and a line here
|
|
40
|
+
* Adding a point is a file in this directory and a line here, points.test.ts fails when a file appears without
|
|
41
41
|
* the line, so the pair cannot come apart. */
|
|
42
42
|
export const CONTRIBUTION_POINTS = [
|
|
43
43
|
viewsPoint,
|
|
@@ -56,13 +56,13 @@ export const CONTRIBUTION_POINTS = [
|
|
|
56
56
|
] as const satisfies readonly ContributionPoint[];
|
|
57
57
|
|
|
58
58
|
// The `contributes` shape those points assemble to: each point's key, its schema, optional. A mapped type
|
|
59
|
-
// rather than a widened record so `manifest.contributes.views` keeps its exact type at every call site
|
|
59
|
+
// rather than a widened record so `manifest.contributes.views` keeps its exact type at every call site, the
|
|
60
60
|
// whole point of the schema being typed at all.
|
|
61
61
|
type ContributesShape = {
|
|
62
62
|
[Point in (typeof CONTRIBUTION_POINTS)[number] as Point["name"]]: z.ZodOptional<Point["schema"]>;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
/* The `contributes` object, assembled rather than hand-written
|
|
65
|
+
/* The `contributes` object, assembled rather than hand-written, which is what makes adding a point a file plus
|
|
66
66
|
* a line above, instead of an edit to a schema thirteen unrelated features share.
|
|
67
67
|
*
|
|
68
68
|
* Each point's description rides `z.describe` onto its own key, so it survives into the generated authoring
|
package/src/points/listener.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
|
-
// One narrowing field the generic automation editor draws for a source
|
|
4
|
+
// One narrowing field the generic automation editor draws for a source, a channel, a branch. `hint` is the
|
|
5
5
|
// sentence under the input, for a filter whose empty case is easy to get wrong.
|
|
6
6
|
const TriggerFieldContributionSchema = z.object({
|
|
7
7
|
label: z.string().min(1),
|
|
@@ -15,12 +15,12 @@ const TriggerFieldContributionSchema = z.object({
|
|
|
15
15
|
* the provider extension is what lets a newly installed listener become configurable without a matching app
|
|
16
16
|
* release.
|
|
17
17
|
*
|
|
18
|
-
* The daemon serves a provider-scoped control surface
|
|
18
|
+
* The daemon serves a provider-scoped control surface. GET /listeners/<provider>/state to reconcile, POST
|
|
19
19
|
* …/dispatch to wake an automation (optionally holding a turn-stream), …/failure + …/status to report. The
|
|
20
20
|
* daemon holds no provider connection itself.
|
|
21
21
|
*
|
|
22
22
|
* WHAT DISPATCHES IT IS OPEN. A gateway process (contributes.processes) is the usual answer and the one the
|
|
23
|
-
* reconcile feed is shaped for
|
|
23
|
+
* reconcile feed is shaped for, it holds a live connection the daemon must not. But an extension BACKEND can
|
|
24
24
|
* dispatch through the same route by declaring the dispatch path in `permissions.daemon`, which is
|
|
25
25
|
* how an area that learns things on its own schedule (an estate poller noticing a container died) contributes
|
|
26
26
|
* a trigger without running a gateway at all. Declaring this with neither is legal and inert: the source is
|
|
@@ -55,7 +55,7 @@ export const ListenerContributionSchema = z.object({
|
|
|
55
55
|
"Only for a source whose message events distinguish being addressed. Absent ⇒ the editor offers no mention-only filter, rather than inventing semantics you did not promise.",
|
|
56
56
|
),
|
|
57
57
|
channel: TriggerFieldContributionSchema.describe("The primary narrowing filter — a channel, a room, a repo."),
|
|
58
|
-
// A SECOND narrowing axis, for a source whose events carry one
|
|
58
|
+
// A SECOND narrowing axis, for a source whose events carry one, a pipeline's git ref, so a trigger can
|
|
59
59
|
// say "the branch that ships" rather than "every agent's every failure". Absent ⇒ the editor offers
|
|
60
60
|
// only the channel filter.
|
|
61
61
|
branchField: TriggerFieldContributionSchema.optional().describe(
|
package/src/points/viewers.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
4
|
/* A custom file viewer the extension may register at runtime (api.viewers.register): the host resolves an open
|
|
5
|
-
* file to this viewer by extension, gets its content, and renders the registered component with it
|
|
5
|
+
* file to this viewer by extension, gets its content, and renders the registered component with it, the host
|
|
6
6
|
* keeps the fetch + open-file lifecycle and the daemon credentials; the extension only renders. This is the
|
|
7
7
|
* non-sidebar contribution point. */
|
|
8
8
|
export const ViewerContributionSchema = z.object({
|
|
@@ -12,12 +12,12 @@ export const ViewerContributionSchema = z.object({
|
|
|
12
12
|
.min(1)
|
|
13
13
|
.describe('Bare file extensions, no dot — e.g. ["docx", "xlsx"].'),
|
|
14
14
|
/* `fetch` is how much of the file the host puts in the extension's hands, and it is a real choice:
|
|
15
|
-
* text
|
|
16
|
-
* blob
|
|
15
|
+
* text, decoded utf8 (`text` prop). For a format that IS text: svg, a subtitle track, a notebook.
|
|
16
|
+
* blob, the whole file in memory (`blob` prop). For a format that must be parsed end to end before any of
|
|
17
17
|
* it can be shown: a .docx, a spreadsheet. Bounded by the daemon's raw-read cap.
|
|
18
|
-
* url
|
|
18
|
+
* url , a streaming URL the component points an element at (`src` prop), never the bytes. For anything
|
|
19
19
|
* RANGE-READ rather than parsed: audio and video, where the file may be gigabytes and the player
|
|
20
|
-
* wants the header, the index and the seconds around the playhead
|
|
20
|
+
* wants the header, the index and the seconds around the playhead, not the file. The host mints
|
|
21
21
|
* the credential on that URL and keeps it out of the extension.
|
|
22
22
|
*/
|
|
23
23
|
fetch: z
|
package/src/powers-diff.ts
CHANGED
|
@@ -2,18 +2,18 @@ import type { ExtensionManifest } from "./manifest.js";
|
|
|
2
2
|
|
|
3
3
|
/* WHAT AN UPDATE ASKS FOR, MECHANICALLY. The install dialog renders a manifest's contributions once; an update
|
|
4
4
|
* is judged on what sits BETWEEN two manifests, and "read both and compare" is exactly the job a person will
|
|
5
|
-
* skip on the fifth update. So each manifest is folded to a set of POWERS
|
|
5
|
+
* skip on the fifth update. So each manifest is folded to a set of POWERS, the consequential facts an owner
|
|
6
6
|
* approved: which daemon routes it may call, which processes the daemon runs for it, what lands on the agent's
|
|
7
|
-
* PATH, what may interrupt from another screen
|
|
7
|
+
* PATH, what may interrupt from another screen, each under a stable key (the identity compared) with a plain
|
|
8
8
|
* sentence (what the reader sees). The diff is set arithmetic over the keys.
|
|
9
9
|
*
|
|
10
10
|
* Deliberately NOT here: plain settings (a new knob is config surface, not reach), display marks, category,
|
|
11
|
-
* version. A power's INTERNALS moving (a process's command line, a fragment's contents) keeps its key
|
|
11
|
+
* version. A power's INTERNALS moving (a process's command line, a fragment's contents) keeps its key, the
|
|
12
12
|
* code changed, which is what the sha pin and the agent diff-read answer for; this diff answers only "did the
|
|
13
13
|
* set of things I approved grow". An empty `added` is what makes an update one click. */
|
|
14
14
|
|
|
15
15
|
export interface PowersDiff {
|
|
16
|
-
// Powers the new manifest declares that the installed one didn't
|
|
16
|
+
// Powers the new manifest declares that the installed one didn't, the reason an update re-asks.
|
|
17
17
|
readonly added: string[];
|
|
18
18
|
readonly removed: string[];
|
|
19
19
|
readonly unchanged: string[];
|
|
@@ -81,7 +81,7 @@ const powersOf = (manifest: ExtensionManifest): Map<string, string> => {
|
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
// `before` absent covers a first install: everything the manifest declares is `added`, which is exactly what
|
|
84
|
-
// the install dialog already renders
|
|
84
|
+
// the install dialog already renders, one vocabulary for both moments.
|
|
85
85
|
export const diffPowers = (before: ExtensionManifest | undefined, after: ExtensionManifest): PowersDiff => {
|
|
86
86
|
const from = before === undefined ? new Map<string, string>() : powersOf(before);
|
|
87
87
|
const to = powersOf(after);
|