@intentic/extension-manifest 1.248.0 → 1.250.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/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js.map +1 -1
- package/dist/contribution-point.d.ts.map +1 -1
- package/dist/json-schema.d.ts.map +1 -1
- package/dist/json-schema.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/mark.d.ts.map +1 -1
- package/dist/mark.js.map +1 -1
- package/dist/points/automation-templates.d.ts.map +1 -1
- package/dist/points/automation-templates.js.map +1 -1
- package/dist/points/capabilities.d.ts.map +1 -1
- package/dist/points/capabilities.js.map +1 -1
- package/dist/points/commands.d.ts.map +1 -1
- package/dist/points/commands.js.map +1 -1
- package/dist/points/documents.d.ts.map +1 -1
- package/dist/points/documents.js.map +1 -1
- package/dist/points/environment.d.ts.map +1 -1
- package/dist/points/environment.js.map +1 -1
- package/dist/points/files.d.ts.map +1 -1
- package/dist/points/files.js.map +1 -1
- package/dist/points/index.d.ts.map +1 -1
- package/dist/points/index.js.map +1 -1
- package/dist/points/listener.d.ts.map +1 -1
- package/dist/points/listener.js.map +1 -1
- package/dist/points/viewers.d.ts.map +1 -1
- package/dist/points/viewers.js.map +1 -1
- package/dist/powers-diff.d.ts.map +1 -1
- package/dist/powers-diff.js.map +1 -1
- package/package.json +3 -3
- package/src/bundle.ts +7 -17
- package/src/contribution-point.ts +6 -18
- package/src/json-schema.ts +9 -24
- package/src/manifest.ts +16 -45
- package/src/mark.ts +8 -44
- package/src/points/automation-templates.ts +12 -25
- package/src/points/capabilities.ts +43 -137
- package/src/points/commands.ts +4 -12
- package/src/points/documents.ts +3 -10
- package/src/points/environment.ts +4 -11
- package/src/points/files.ts +7 -26
- package/src/points/index.ts +7 -20
- package/src/points/listener.ts +11 -23
- package/src/points/viewers.ts +9 -13
- package/src/powers-diff.ts +7 -14
package/src/mark.ts
CHANGED
|
@@ -1,50 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* Three tiers, and none is required. `art` is the extension's OWN drawing, carried inline: the only tier an
|
|
8
|
-
* author controls completely, and the only one that can make a grid of unfamiliar names look like a shelf of
|
|
9
|
-
* distinct products rather than a column of identical glyphs. `logo` is a simple-icons slug fetched from a
|
|
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, so it can never
|
|
12
|
-
* be the only tier. `icon` is a name from the host's own bundled vocabulary, which ships in the image, follows
|
|
13
|
-
* the theme and costs no request; it is what carries a first-party extension that has not been drawn yet. What
|
|
14
|
-
* declares none of them is drawn as its initials, so no row is ever blank and no author is obliged to have a
|
|
15
|
-
* brand or an illustrator.
|
|
16
|
-
*
|
|
17
|
-
* ART BEATS A BRAND SLUG because they answer different questions. A slug says "this is Slack"; art says "this
|
|
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, but where both arrive, the author's own drawing is the more
|
|
20
|
-
* specific claim and wins.
|
|
21
|
-
*
|
|
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, the rule the rail already applies to Activation.icon, here for
|
|
24
|
-
* the surfaces that must draw an extension whose code is not running: one that is switched off, one that is
|
|
25
|
-
* daemon-only, one being read about in a registry before it is installed at all. */
|
|
3
|
+
// The mark a capability card or extension draws itself with, one shape since one component (<BrandMark>) draws both:
|
|
4
|
+
// `art` (inline SVG, own drawing) beats `logo` (simple-icons slug) beats `icon` (host's set), falling back to initials
|
|
5
|
+
// when none is declared.
|
|
26
6
|
|
|
27
|
-
|
|
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
|
-
* whole official registry's artwork costs less than one screenshot, and an author who needs more than that is
|
|
30
|
-
* shipping a raster they should be shipping as a brand slug or not at all. */
|
|
7
|
+
// Big enough for a drawn mark, far too small for a traced photograph.
|
|
31
8
|
const ART_MAX_BYTES = 4096;
|
|
32
9
|
|
|
33
10
|
export const MARK_FIELDS = {
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* A URL would put a stranger's server in the render path of a page listing extensions, a fetch that
|
|
37
|
-
* tracks who is browsing what, breaks in an offline sandbox, and 404s long after the row was approved:
|
|
38
|
-
* the three failures the `logo` tier already documents, with none of its excuse. Inline costs one string
|
|
39
|
-
* and always draws.
|
|
40
|
-
*
|
|
41
|
-
* Kept as READABLE TEXT rather than a data URI because of who reads it. The registry's curated file is
|
|
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, an opaque blob in the one file whose entire purpose is
|
|
44
|
-
* to be checked. The renderer does its own encoding at the point of use.
|
|
45
|
-
*
|
|
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, see <BrandMark>, which owns that guarantee and the sniff test that enforces it. */
|
|
11
|
+
// SVG document itself, not a URL or base64; drawn inert (<img>, never inlined) so a row can't script the page.
|
|
48
12
|
art: z
|
|
49
13
|
.string()
|
|
50
14
|
.max(ART_MAX_BYTES)
|
|
@@ -52,15 +16,15 @@ export const MARK_FIELDS = {
|
|
|
52
16
|
.describe(
|
|
53
17
|
"This extension's own mark, as a complete SVG document inline: the tier an author controls fully. Give it a viewBox and let it fill its own square edge to edge; it is drawn as the tile, not as a glyph on a plate. Kept as readable SVG text (not base64) so a registry reviewer can see what they are publishing, drawn inert so it cannot script the page, and capped at 4 KB. Anything that does not parse as SVG falls back to `logo`, then `icon`, then initials.",
|
|
54
18
|
),
|
|
55
|
-
// A simple-icons slug (https://cdn.simpleicons.org/<slug>). A "/<hex>" suffix forces a colour for
|
|
56
|
-
//
|
|
19
|
+
// A simple-icons slug (https://cdn.simpleicons.org/<slug>). A "/<hex>" suffix forces a colour for a mark that
|
|
20
|
+
// vanishes against its surface.
|
|
57
21
|
logo: z
|
|
58
22
|
.string()
|
|
59
23
|
.optional()
|
|
60
24
|
.describe(
|
|
61
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.',
|
|
62
26
|
),
|
|
63
|
-
// A name from the host's icon set (@intentic/ui IconName), drawn when no simple-icons slug fits.
|
|
27
|
+
// A name from the host's own icon set (@intentic/ui IconName), drawn when no simple-icons slug fits.
|
|
64
28
|
icon: z
|
|
65
29
|
.string()
|
|
66
30
|
.optional()
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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. */
|
|
4
|
+
// A starting point in the automation composer: a trigger, a prompt written for that trigger's payload, and any guard
|
|
5
|
+
// that makes it safe to leave on. Pure prefill, creating one makes an ordinary automation. The daemon validates each
|
|
6
|
+
// against the real trigger schema when building the catalogue and drops what doesn't parse.
|
|
16
7
|
export const AutomationTemplateContributionSchema = z.object({
|
|
17
|
-
// Prefills the automation name
|
|
18
|
-
// spelled as an automation id, not as prose.
|
|
8
|
+
// Prefills the automation name and is what "does one exist already" is asked by, so spell it as an id, not prose.
|
|
19
9
|
id: z
|
|
20
10
|
.string()
|
|
21
11
|
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/)
|
|
@@ -23,16 +13,16 @@ export const AutomationTemplateContributionSchema = z.object({
|
|
|
23
13
|
title: z.string().min(1),
|
|
24
14
|
logo: z.string().min(1).optional().describe("A simple-icons slug for the card."),
|
|
25
15
|
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
|
|
27
|
-
//
|
|
16
|
+
// Capability providers that make this template work; any one connected is enough. Omitted ⇒ nothing to connect,
|
|
17
|
+
// always offered.
|
|
28
18
|
requires: z
|
|
29
19
|
.array(z.string().min(1))
|
|
30
20
|
.optional()
|
|
31
21
|
.describe(
|
|
32
22
|
"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
23
|
),
|
|
34
|
-
// Shaped loosely here and parsed strictly at the merge
|
|
35
|
-
//
|
|
24
|
+
// Shaped loosely here and parsed strictly at the merge, since this package can't see the daemon's real trigger
|
|
25
|
+
// union.
|
|
36
26
|
trigger: z
|
|
37
27
|
.object({
|
|
38
28
|
kind: z.enum(["schedule", "event", "listener", "workspace"]),
|
|
@@ -54,19 +44,16 @@ export const AutomationTemplateContributionSchema = z.object({
|
|
|
54
44
|
note: z.string().min(1).optional(),
|
|
55
45
|
setup: z.string().min(1).optional().describe("What the user must do themselves before this can work."),
|
|
56
46
|
description: z.string().min(1).optional(),
|
|
57
|
-
|
|
58
|
-
|
|
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. */
|
|
47
|
+
// Absent ⇒ waits in the gallery. `create` puts a card that makes it switched off in one click; `configure` opens
|
|
48
|
+
// the dialog prefilled, for a template that can't work unconfigured.
|
|
62
49
|
offer: z
|
|
63
50
|
.enum(["create", "configure"])
|
|
64
51
|
.optional()
|
|
65
52
|
.describe(
|
|
66
53
|
"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
54
|
),
|
|
68
|
-
// Whether what this makes watches
|
|
69
|
-
//
|
|
55
|
+
// Whether what this makes watches this codebase rather than the outside world; declared rather than read off the
|
|
56
|
+
// trigger, since a schedule can point either way.
|
|
70
57
|
chore: z
|
|
71
58
|
.boolean()
|
|
72
59
|
.optional()
|
|
@@ -3,9 +3,8 @@ import { z } from "zod";
|
|
|
3
3
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
4
4
|
import { MARK_FIELDS } from "../mark.js";
|
|
5
5
|
|
|
6
|
-
// A field the
|
|
7
|
-
//
|
|
8
|
-
// contributed cards from installed extensions exactly like core capability cards.
|
|
6
|
+
// A field the install dialog's config form renders (key, label, secret/optional flags, a select, a `when` gate);
|
|
7
|
+
// mirrors the platform catalog's own field shape.
|
|
9
8
|
export const CapabilityFieldSchema = z.object({
|
|
10
9
|
key: z.string().regex(/^[a-zA-Z][a-zA-Z0-9]*$/),
|
|
11
10
|
label: z.string().min(1),
|
|
@@ -13,44 +12,31 @@ export const CapabilityFieldSchema = z.object({
|
|
|
13
12
|
secret: z.boolean().optional().describe("Mask it, and never echo it back."),
|
|
14
13
|
optional: z.boolean().optional(),
|
|
15
14
|
multiline: z.boolean().optional(),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* everyone was spending that cost on nearly everyone. The disclosure opens BY ITSELF while any advanced
|
|
19
|
-
* field holds a non-default value, so an edit never hides the settings it is standing on. */
|
|
15
|
+
// Folds this field behind the form's Advanced disclosure; it opens by itself while any advanced field holds a
|
|
16
|
+
// non-default value.
|
|
20
17
|
advanced: z
|
|
21
18
|
.boolean()
|
|
22
19
|
.optional()
|
|
23
20
|
.describe(
|
|
24
21
|
"Fold this field behind the form's Advanced disclosure: for answers whose default is right for nearly everyone. The disclosure opens by itself while any advanced field holds a non-default value, so an edit never hides live settings.",
|
|
25
22
|
),
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// value, and reads wrong for this: it presents a choice the user must make to proceed, sized like the
|
|
29
|
-
// required fields around it. Something the capability works fine without wants the control that is quiet
|
|
30
|
-
// when it is off. A switch always holds one of its two values, so such a field never blocks a submit
|
|
31
|
-
// whatever `optional` says.
|
|
23
|
+
// Renders as a switch carrying "on"/"off", for an opt-in extra rather than a required decision; always holds a
|
|
24
|
+
// value, so it never blocks a submit.
|
|
32
25
|
boolean: z
|
|
33
26
|
.boolean()
|
|
34
27
|
.optional()
|
|
35
28
|
.describe(
|
|
36
29
|
'Render it as a switch, carrying "on"/"off". For an opt-in EXTRA rather than a decision: a two-option picker says the same thing but presents a choice the user must make to proceed, sized like the required fields around it. A switch always holds a value, so a field like this never blocks a submit.',
|
|
37
30
|
),
|
|
38
|
-
// A line under the control
|
|
39
|
-
// value takes effect). The card's `hint` speaks for the whole card; this one is bound to the field it
|
|
40
|
-
// qualifies, which is where a per-option caveat has to sit to be read at all.
|
|
31
|
+
// A line under the control for what the label alone can't say (a host requirement, when a value takes effect).
|
|
41
32
|
hint: z
|
|
42
33
|
.string()
|
|
43
34
|
.optional()
|
|
44
35
|
.describe(
|
|
45
36
|
"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.",
|
|
46
37
|
),
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* The one fact a user needs before touching a control, and the one the form cannot infer: two switches
|
|
51
|
-
* side by side, identical in every visible way, can cost five seconds and five minutes. The docker card
|
|
52
|
-
* has exactly that pair (its GPU option is baked into the image; its engine options are a file dockerd
|
|
53
|
-
* rereads), and without this flag the only way to find out which you pressed is to press it. */
|
|
38
|
+
// Value only takes effect after a sandbox rebuild (rides the image overlay); shown as a chip since two
|
|
39
|
+
// otherwise-identical switches can cost very differently.
|
|
54
40
|
rebuild: z
|
|
55
41
|
.boolean()
|
|
56
42
|
.optional()
|
|
@@ -62,13 +48,7 @@ export const CapabilityFieldSchema = z.object({
|
|
|
62
48
|
.array(z.object({ value: z.string(), label: z.string() }))
|
|
63
49
|
.optional()
|
|
64
50
|
.describe("Turns the field into a select."),
|
|
65
|
-
|
|
66
|
-
* chosen, the gateway fields that belong to one VPN provider. A `when` condition (@intentic/base/when)
|
|
67
|
-
* evaluated against the form's live values, so it re-reads as the user toggles.
|
|
68
|
-
*
|
|
69
|
-
* Refused at parse when it does not parse. A card is data an extension ships, and a condition nobody can
|
|
70
|
-
* evaluate is not a field that is always shown or always hidden, it is a card whose author believes it
|
|
71
|
-
* asks something it never asks. Failing the manifest names the card; failing at render names nothing. */
|
|
51
|
+
// Gates the field on answers already given; refused at parse when the `when` expression doesn't parse.
|
|
72
52
|
when: z
|
|
73
53
|
.string()
|
|
74
54
|
.refine(isWhenExpression, { message: "not a valid `when` condition" })
|
|
@@ -76,19 +56,15 @@ export const CapabilityFieldSchema = z.object({
|
|
|
76
56
|
.describe(
|
|
77
57
|
"Only show this field while a condition over the answers already given holds: `auth == 'key'`, `provider in ['ipsec', 'fortinet']`, `!advanced`. Supports `&&`, `||`, `!`, comparisons and `in`.",
|
|
78
58
|
),
|
|
79
|
-
// A fixed value baked into the config rather than asked for,
|
|
80
|
-
// (platform="reddit", provider="stripe"). Rendered as nothing; sent as itself.
|
|
59
|
+
// A fixed value baked into the config rather than asked for, e.g. pinning a discriminator; renders as nothing.
|
|
81
60
|
value: z
|
|
82
61
|
.string()
|
|
83
62
|
.optional()
|
|
84
63
|
.describe(
|
|
85
64
|
'A fixed value baked into the config rather than asked for: how a card pins its discriminator (platform="reddit", provider="stripe"). Renders as nothing.',
|
|
86
65
|
),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
* echoed and, unlike an ordinary secret, never enters the agent's environment either, the daemon mints the
|
|
90
|
-
* six-digit codes on demand (`otp <name>` / GET /capabilities/<id>/otp) and only those cross, each dead
|
|
91
|
-
* within its period. A cli entry whose env references a totp field therefore fails to parse (see below). */
|
|
66
|
+
// Marks this field as a TOTP seed; declare with `secret: true`. Unlike an ordinary secret it never reaches the
|
|
67
|
+
// agent's environment, the daemon mints codes on demand.
|
|
92
68
|
totp: z
|
|
93
69
|
.boolean()
|
|
94
70
|
.optional()
|
|
@@ -98,30 +74,13 @@ export const CapabilityFieldSchema = z.object({
|
|
|
98
74
|
});
|
|
99
75
|
export type CapabilityField = z.infer<typeof CapabilityFieldSchema>;
|
|
100
76
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* Two tiers ask this question about the same card. The web's form asks it to decide what to draw and what to
|
|
104
|
-
* validate; the daemon asks it at install to decide which fields it may demand. They used to answer it with
|
|
105
|
-
* their own copies of the same comparison, in different packages, and the answers only had to diverge once for
|
|
106
|
-
* a card to become unusable in exactly one direction: a field the form never showed, refused at submit for
|
|
107
|
-
* being empty. Nothing in either copy referred to the other, so the divergence would have arrived as a bug
|
|
108
|
-
* report about one card rather than as a broken rule.
|
|
109
|
-
*
|
|
110
|
-
* It lives beside the schema for the reason the description does: this is a fact about the shape, and the two
|
|
111
|
-
* consumers are in different packages. Parsed per call rather than cached, a card has a handful of fields,
|
|
112
|
-
* this runs on a keystroke at worst, and a cache keyed by manifest strings is a map that outlives every
|
|
113
|
-
* extension that ever declared one. */
|
|
77
|
+
// Whether a field is in play given the answers so far. The web form and the daemon's install-time check both need this
|
|
78
|
+
// same answer.
|
|
114
79
|
export const fieldApplies = (field: CapabilityField, values: Readonly<Record<string, unknown>>): boolean =>
|
|
115
80
|
field.when === undefined || evaluateWhen(parseWhen(field.when), values);
|
|
116
81
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
* and the machinery is not.
|
|
120
|
-
*
|
|
121
|
-
* It is worth declaring on any card whose failure is otherwise silent, which is most of them: a wrong token, a
|
|
122
|
-
* host the sandbox cannot route to and a service that is simply not running all present identically as a card
|
|
123
|
-
* that says "not connected" some time after the form was left. The probe turns each into a sentence on the form
|
|
124
|
-
* the reader is still standing on. */
|
|
82
|
+
// One authenticated request the daemon makes on the form's behalf, declared as data since the check is per-service.
|
|
83
|
+
// Worth declaring on any card whose failure is otherwise silent.
|
|
125
84
|
const ProbeSchema = z.object({
|
|
126
85
|
url: z
|
|
127
86
|
.string()
|
|
@@ -132,9 +91,7 @@ const ProbeSchema = z.object({
|
|
|
132
91
|
.record(z.string(), z.string())
|
|
133
92
|
.optional()
|
|
134
93
|
.describe('The request headers, templated the same way: `{"Authorization": "Bearer ${token}"}`.'),
|
|
135
|
-
|
|
136
|
-
* as ada" is a different fact from "200 OK", and it is the one that tells a reader they connected the
|
|
137
|
-
* account they meant to. A dotted path; a miss just leaves the message general. */
|
|
94
|
+
// Dotted path into the JSON answer naming who the caller is, so success can say which account answered.
|
|
138
95
|
identity: z
|
|
139
96
|
.string()
|
|
140
97
|
.optional()
|
|
@@ -145,16 +102,11 @@ const ProbeSchema = z.object({
|
|
|
145
102
|
.describe("Accept a self-signed certificate, for a service whose local install ships one (Obsidian's Local REST API)."),
|
|
146
103
|
});
|
|
147
104
|
|
|
148
|
-
// The
|
|
149
|
-
// Shared by every arm below, because none of that varies with the kind.
|
|
105
|
+
// The install-dialog card: how it looks in the grid and how the user gets its credential. Shared by every kind below.
|
|
150
106
|
const CatalogSchema = z.object({
|
|
151
107
|
name: z.string().min(1),
|
|
152
108
|
...MARK_FIELDS,
|
|
153
|
-
//
|
|
154
|
-
// others in a pane the index column has already taken 16rem out of, so a paragraph here is a paragraph the
|
|
155
|
-
// reader gets truncated. Everything longer belongs in `hint`, which the config form prints in full and the
|
|
156
|
-
// catalog's search reads. Not capped in the schema: an extension published before this rule should still
|
|
157
|
-
// install, and a card that reads badly is a worse outcome than one that fails to load only in theory.
|
|
109
|
+
// One line, aim for 60 characters or fewer; the grid clamps this at two lines. Anything longer belongs in `hint`.
|
|
158
110
|
description: z
|
|
159
111
|
.string()
|
|
160
112
|
.min(1)
|
|
@@ -162,15 +114,15 @@ const CatalogSchema = z.object({
|
|
|
162
114
|
"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`.",
|
|
163
115
|
),
|
|
164
116
|
category: z.string().min(1),
|
|
165
|
-
// The paragraph
|
|
166
|
-
//
|
|
117
|
+
// The paragraph shown under the add form and searched from the catalog; words someone would search for belong here
|
|
118
|
+
// even if the tile can't show them.
|
|
167
119
|
hint: z
|
|
168
120
|
.string()
|
|
169
121
|
.optional()
|
|
170
122
|
.describe(
|
|
171
123
|
'The paragraph, shown under the add form and searched from the catalog, so the words that identify this card to someone hunting for it ("webauthn", "socket mode") belong here even when the tile cannot show them.',
|
|
172
124
|
),
|
|
173
|
-
// The credential-creation walkthrough the install dialog renders
|
|
125
|
+
// The credential-creation walkthrough the install dialog renders.
|
|
174
126
|
guide: z
|
|
175
127
|
.object({
|
|
176
128
|
url: z.string().optional(),
|
|
@@ -184,33 +136,20 @@ const CatalogSchema = z.object({
|
|
|
184
136
|
.describe("The walkthrough the install dialog renders for getting the credential this card asks for."),
|
|
185
137
|
});
|
|
186
138
|
|
|
187
|
-
// Every arm carries these: the slug
|
|
188
|
-
//
|
|
139
|
+
// Every arm carries these: the slug (the card's /capabilities/<id> route and the discriminator value the daemon
|
|
140
|
+
// resolves), the card, and its fields.
|
|
189
141
|
const contributionBase = {
|
|
190
142
|
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
191
143
|
catalog: CatalogSchema,
|
|
192
144
|
fields: z.array(CapabilityFieldSchema),
|
|
193
145
|
};
|
|
194
146
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* kinds NOT listed here are the ones whose card is one-to-one with a handler that owns real privilege (`docker`
|
|
198
|
-
* bakes --privileged, `vpn` bakes NET_ADMIN, `extension` installs extensions, `devops` scaffolds repos): their
|
|
199
|
-
* cards live in the platform catalog because separating card from handler would split one concept in two, and
|
|
200
|
-
* because a manifest that could name them would be a manifest that grants itself privilege. That restriction is
|
|
201
|
-
* this discriminated union, not a comment, a manifest naming any other kind fails to parse.
|
|
202
|
-
*
|
|
203
|
-
* `${id}` in a cli/host skill file is substituted with the instance name at apply time (so a host pack's tool
|
|
204
|
-
* names read `mcp__my-laptop__run_command`), and, for `cli`, each `$ENVVAR` becomes its per-instance suffixed
|
|
205
|
-
* name. A BROWSER pack's skill renders once per SITE rather than per instance, its seams are `${accounts}`
|
|
206
|
-
* (the roster of connected accounts), `${tools}` (the core driving/connecting note) and `${site}` (the host,
|
|
207
|
-
* for the generic card whose text can name no site of its own); `${id}` and per-field substitution do not
|
|
208
|
-
* apply there (capabilities/account-skills.ts in the sandbox daemon). */
|
|
147
|
+
// A capability card as data; kinds with genuine device privilege (docker, vpn, extension, devops) live in the platform
|
|
148
|
+
// catalog instead, since a manifest that could name them would grant itself that privilege.
|
|
209
149
|
export const CapabilityContributionSchema = z
|
|
210
150
|
.discriminatedUnion("kind", [
|
|
211
|
-
// A CLI tool the
|
|
212
|
-
//
|
|
213
|
-
// image fragment holding the client binary (psql, mysql, whisper).
|
|
151
|
+
// A CLI tool the agent gets, authenticated: env vars templated over the fields, a SKILL.md cheatsheet, and an
|
|
152
|
+
// optional image fragment for the client binary.
|
|
214
153
|
z.object({
|
|
215
154
|
...contributionBase,
|
|
216
155
|
kind: z.literal("cli"),
|
|
@@ -229,13 +168,8 @@ export const CapabilityContributionSchema = z
|
|
|
229
168
|
.min(1)
|
|
230
169
|
.optional()
|
|
231
170
|
.describe("A Dockerfile fragment holding the client binary this tool needs (psql, mysql, whisper)."),
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
* capability enables with no overlay and no rebuild at all, and on one that doesn't, the overlay
|
|
235
|
-
* gets the pack's own fragment — the same bytes the image would have baked, never a second copy
|
|
236
|
-
* that can drift from it. Discord's voice transcription was exactly that second copy: a fragment
|
|
237
|
-
* whose RUN lines were byte-identical to the `whisper` pack, so a standard image compiled
|
|
238
|
-
* whisper.cpp once at publish and again in every overlay rebuild, for the same binary. */
|
|
171
|
+
// Prefer over `fragment` whenever the sandbox already ships a pack for the tool: an image that bakes the
|
|
172
|
+
// pack needs no rebuild, and the overlay can't drift from it.
|
|
239
173
|
pack: z
|
|
240
174
|
.string()
|
|
241
175
|
.min(1)
|
|
@@ -247,21 +181,8 @@ export const CapabilityContributionSchema = z
|
|
|
247
181
|
"One authenticated request that tests this card's settings before they are saved, so a wrong token or an unreachable host is answered on the form rather than by a card that says 'not connected' afterwards.",
|
|
248
182
|
),
|
|
249
183
|
}),
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
* opens once it HAS one, the owner's own hands on the connected browser, which a login page is the wrong
|
|
253
|
-
* place to start (signed in, it only redirects). Two fields because for some platforms the login lives on
|
|
254
|
-
* another site entirely (YouTube signs in at accounts.google.com), so one cannot be derived from the other.
|
|
255
|
-
* No `env` and no `fragment`: the browser itself is core (one Chromium install serves every platform),
|
|
256
|
-
* only the identity is per-entry. A card never declares the account's username/password either, those are
|
|
257
|
-
* CORE form fields on every browser card (the catalog appends them; the daemon's add validation accepts
|
|
258
|
-
* them), because which box a login form wants filled is the same fact on every site.
|
|
259
|
-
*
|
|
260
|
-
* BOTH URLs ARE OPTIONAL, so that one card can be the GENERIC one: a site card pins them (Reddit knows
|
|
261
|
-
* where Reddit signs in), and the generic "browser session" card asks for them on its form instead, which
|
|
262
|
-
* is what lets a user connect a site nobody shipped a card for. A card must do one or the other, pin a
|
|
263
|
-
* URL or declare a field that supplies it, and the daemon's apply says so on the form when neither does,
|
|
264
|
-
* because the alternative is a sign-in window that opens on nothing. */
|
|
184
|
+
// A site the agent acts on as the owner through the shared browser; `loginUrl`/`homeUrl` are both optional so a
|
|
185
|
+
// card can be the generic one that asks for them on its form instead.
|
|
265
186
|
z.object({
|
|
266
187
|
...contributionBase,
|
|
267
188
|
kind: z.literal("browser"),
|
|
@@ -284,36 +205,27 @@ export const CapabilityContributionSchema = z
|
|
|
284
205
|
"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}`.",
|
|
285
206
|
),
|
|
286
207
|
}),
|
|
287
|
-
// An operating system a connected computer can run,
|
|
288
|
-
//
|
|
208
|
+
// An operating system a connected computer can run; enrollment, socket and scope enforcement are core, only the
|
|
209
|
+
// skill pack varies.
|
|
289
210
|
z.object({
|
|
290
211
|
...contributionBase,
|
|
291
212
|
kind: z.literal("host"),
|
|
292
213
|
skill: z.string().min(1).describe("Checkout-relative SKILL.md teaching the agent that machine's shell."),
|
|
293
214
|
}),
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
* core (and, for the grants, the browser's own), so a card supplies exactly two things a family differs
|
|
297
|
-
* in — where its extension is installed from, and how its skill pack words the difference.
|
|
298
|
-
*
|
|
299
|
-
* `install` is a URL rather than a store id because the families do not share a store: Chrome, Edge and
|
|
300
|
-
* Firefox each have their own, and a self-hosted build is a zip on a page. The card renders it as the
|
|
301
|
-
* link in the connect dialog, so an unlisted family is one manifest entry away from working. */
|
|
215
|
+
// A browser family the user connects their own copy of; `install` is a URL since each family has its own store
|
|
216
|
+
// or none at all.
|
|
302
217
|
z.object({
|
|
303
218
|
...contributionBase,
|
|
304
219
|
kind: z.literal("webext"),
|
|
305
220
|
install: z.url().describe("Where this browser's extension is installed from: its store listing, or a page offering the build."),
|
|
306
221
|
skill: z.string().min(1).describe("Checkout-relative SKILL.md teaching the agent to drive this browser."),
|
|
307
222
|
}),
|
|
308
|
-
|
|
309
|
-
* ACP agent needs is a command, so "OpenCode" is entirely a name, a logo and a filled-in form, which is
|
|
310
|
-
* exactly what a catalog row is. */
|
|
223
|
+
// A preset over a core kind: no payload, just a name, a logo and a filled-in form.
|
|
311
224
|
z.object({ ...contributionBase, kind: z.literal("agent") }),
|
|
312
225
|
])
|
|
313
226
|
.superRefine((spec, ctx) => {
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// agent the second factor itself instead of one expiring code at a time.
|
|
227
|
+
// A totp field's env must never be referenced: that would hand the agent the seed itself instead of the
|
|
228
|
+
// daemon's one-time codes.
|
|
317
229
|
if (spec.kind !== "cli") {
|
|
318
230
|
return;
|
|
319
231
|
}
|
|
@@ -327,17 +239,11 @@ export const CapabilityContributionSchema = z
|
|
|
327
239
|
}
|
|
328
240
|
});
|
|
329
241
|
export type CapabilityContribution = z.infer<typeof CapabilityContributionSchema>;
|
|
330
|
-
//
|
|
242
|
+
// Arms carrying a per-instance SKILL.md, templated and installed identically by the daemon.
|
|
331
243
|
export type SkillContribution = Extract<CapabilityContribution, { skill: string }>;
|
|
332
244
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
* another's. `agent` has none on purpose: its cards are presets over one config shape, differing only in their
|
|
336
|
-
* defaults, so every agent instance belongs to every agent card equally.
|
|
337
|
-
*
|
|
338
|
-
* Here, beside the schema, because it is a fact about the contribution shape, the daemon, the catalog and the
|
|
339
|
-
* web all need it, and three copies of it is three chances for a card's instances to go missing. `satisfies`
|
|
340
|
-
* rather than a lookup table so a new arm above is a compile error until this answers for it. */
|
|
245
|
+
// The config key a kind's cards pin to their own id, so a stored capability traces back to its card. `agent` has none:
|
|
246
|
+
// its cards differ only in defaults.
|
|
341
247
|
const DISCRIMINATOR = { cli: "provider", browser: "platform", host: "platform", webext: "platform", agent: undefined } satisfies Record<
|
|
342
248
|
CapabilityContribution["kind"],
|
|
343
249
|
string | undefined
|
package/src/points/commands.ts
CHANGED
|
@@ -7,10 +7,8 @@ export const CommandContributionSchema = z.object({
|
|
|
7
7
|
command: z.string().regex(/^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/),
|
|
8
8
|
title: z.string().min(1).describe("What the command palette shows. The manifest's value wins over the one passed at registration."),
|
|
9
9
|
icon: z.string().optional().describe("A name from the host's icon set, drawn beside the title."),
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// approval surface, a global shortcut is consequential, so like title/icon the manifest value is authoritative
|
|
13
|
-
// and the host binds only what was approved. Whitespace-free; an unparseable chord simply never fires.
|
|
10
|
+
// Global keyboard shortcut in the host's chord notation. Declared here so it rides the install dialog's approval;
|
|
11
|
+
// the host binds only what was approved.
|
|
14
12
|
keybinding: z
|
|
15
13
|
.string()
|
|
16
14
|
.regex(/^\S+$/)
|
|
@@ -18,14 +16,8 @@ export const CommandContributionSchema = z.object({
|
|
|
18
16
|
.describe(
|
|
19
17
|
'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
18
|
),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* name, and what a condition decides is whether the chord is claimed from whatever else would have had it.
|
|
24
|
-
*
|
|
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, so every extension command took its chord
|
|
27
|
-
* globally, from every surface, including terminals where a bare key belongs to the program on the other
|
|
28
|
-
* end. A condition an extension can write is what makes a contributed shortcut safe to grant. */
|
|
19
|
+
// When the keybinding applies, as a condition over the shell's context keys; the palette ignores it since a command
|
|
20
|
+
// is always runnable by name. Without one the chord is claimed everywhere, including inside a terminal.
|
|
29
21
|
when: z
|
|
30
22
|
.string()
|
|
31
23
|
.refine(isWhenExpression, { message: "not a valid `when` condition" })
|
package/src/points/documents.ts
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
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, both are decided in the manifest because the owner must see
|
|
10
|
-
* them. A document provider takes nothing over: it adds an icon to rows it has something for, and every one of
|
|
11
|
-
* those rows is evidence the owner can see for themselves. So the manifest gates WHETHER the extension may mark
|
|
12
|
-
* up the tree at all, and the per-row wording stays with the provider, which is the only thing that knows what
|
|
13
|
-
* it found. */
|
|
4
|
+
// A per-directory document family the extension may register at runtime; the provider marks rows in the Workspace tree
|
|
5
|
+
// it can explain, and the host opens its component as a tab. Only id and label are declared: which rows to mark is the
|
|
6
|
+
// provider's own call, made visibly, so nothing else needs owner approval.
|
|
14
7
|
export const DocumentContributionSchema = z.object({
|
|
15
8
|
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
16
9
|
// The family's human name, shown in the install dialog beside the extension's other contributions.
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ContributionPoint } from "../contribution-point.js";
|
|
3
3
|
|
|
4
|
-
// A Dockerfile fragment baked into the sandbox image overlay so the extension's tools
|
|
5
|
-
//
|
|
6
|
-
// from extension fragments (those stay daemon-owned), and the owner approves the composed overlay + rebuilds
|
|
4
|
+
// A Dockerfile fragment baked into the sandbox image overlay so the extension's tools exist at runtime. The daemon
|
|
5
|
+
// rejects FROM and privileged `# intentic:runtime` directives; the owner approves the composed overlay and rebuild
|
|
7
6
|
// out-of-band.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* fragment or a pack" here means `fragment` stops being required — and that drops `required: ["fragment"]`
|
|
11
|
-
* from the wire contract, which the lock gate correctly reads as something a client could have relied on. No
|
|
12
|
-
* extension needs it yet: the one manifest using this point asks for an npm package, and a connector wanting
|
|
13
|
-
* a packed tool declares it on its `cli` contribution, which is where the duplication this solves came from.
|
|
14
|
-
* Worth adding the day something needs it, as a declared contract change with a Breaking-Note, rather than
|
|
15
|
-
* spending one now on symmetry. */
|
|
7
|
+
// No `pack` field here: expressing "either a fragment or a pack" would make `fragment` optional, which breaks the wire
|
|
8
|
+
// contract's `required` list. Add one when an extension actually needs it.
|
|
16
9
|
export const EnvironmentContributionSchema = z.object({
|
|
17
10
|
fragment: z
|
|
18
11
|
.string()
|