@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.
Files changed (45) hide show
  1. package/dist/bundle.d.ts.map +1 -1
  2. package/dist/bundle.js.map +1 -1
  3. package/dist/contribution-point.d.ts.map +1 -1
  4. package/dist/json-schema.d.ts.map +1 -1
  5. package/dist/json-schema.js.map +1 -1
  6. package/dist/manifest.d.ts.map +1 -1
  7. package/dist/manifest.js.map +1 -1
  8. package/dist/mark.d.ts.map +1 -1
  9. package/dist/mark.js.map +1 -1
  10. package/dist/points/automation-templates.d.ts.map +1 -1
  11. package/dist/points/automation-templates.js.map +1 -1
  12. package/dist/points/capabilities.d.ts.map +1 -1
  13. package/dist/points/capabilities.js.map +1 -1
  14. package/dist/points/commands.d.ts.map +1 -1
  15. package/dist/points/commands.js.map +1 -1
  16. package/dist/points/documents.d.ts.map +1 -1
  17. package/dist/points/documents.js.map +1 -1
  18. package/dist/points/environment.d.ts.map +1 -1
  19. package/dist/points/environment.js.map +1 -1
  20. package/dist/points/files.d.ts.map +1 -1
  21. package/dist/points/files.js.map +1 -1
  22. package/dist/points/index.d.ts.map +1 -1
  23. package/dist/points/index.js.map +1 -1
  24. package/dist/points/listener.d.ts.map +1 -1
  25. package/dist/points/listener.js.map +1 -1
  26. package/dist/points/viewers.d.ts.map +1 -1
  27. package/dist/points/viewers.js.map +1 -1
  28. package/dist/powers-diff.d.ts.map +1 -1
  29. package/dist/powers-diff.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/bundle.ts +7 -17
  32. package/src/contribution-point.ts +6 -18
  33. package/src/json-schema.ts +9 -24
  34. package/src/manifest.ts +16 -45
  35. package/src/mark.ts +8 -44
  36. package/src/points/automation-templates.ts +12 -25
  37. package/src/points/capabilities.ts +43 -137
  38. package/src/points/commands.ts +4 -12
  39. package/src/points/documents.ts +3 -10
  40. package/src/points/environment.ts +4 -11
  41. package/src/points/files.ts +7 -26
  42. package/src/points/index.ts +7 -20
  43. package/src/points/listener.ts +11 -23
  44. package/src/points/viewers.ts +9 -13
  45. 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
- /* HOW SOMETHING LOOKS BEFORE ANY OF ITS CODE RUNS, the mark a capability card and an extension are drawn
4
- * with, in ONE shape because one component draws both (<BrandMark>) and a second copy of these two fields is a
5
- * second answer to what happens when a slug 404s.
6
- *
7
- * 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
- /* Big enough for a drawn mark, far too small for a traced photograph, which is the line being drawn. This
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
- /* THE SVG DOCUMENT ITSELF, not a URL and not base64.
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 marks
56
- // that vanish against the surface they land on (github's near-black).
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
- /* A STARTING POINT in the automation composer, a trigger, a prompt written for that trigger's payload, and
5
- * whatever guard or hold makes it safe to leave on. Pure prefill: creating one makes an ordinary automation and
6
- * the daemon knows nothing about templates afterwards.
7
- *
8
- * IT LIVES WITH THE AREA THAT KNOWS THE SERVICE, which is the point of it being a contribution at all. The
9
- * automation surface used to carry every one of these. Komodo's, Sentry's, Stripe's, CI's, the chore book's,
10
- * so a pack that gained something worth reacting to could not say so without an edit to a surface it has
11
- * nothing to do with. A template declared here appears when the pack is installed and its capability connected,
12
- * and disappears with it.
13
- *
14
- * The daemon validates each one against the real trigger schema when it builds the catalogue and drops what
15
- * does not parse, so a template can never offer a trigger that `upsert` would refuse. */
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, and is what "does one of these exist already" is asked by, so it must be
18
- // spelled as an automation id, not as prose.
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 WORK, any one connected is enough (fixing CI rides github
27
- // or gitlab). Omitted ⇒ nothing to connect, always offered.
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: the manifest package cannot see the trigger union
35
- // (the dependency runs the other way), so the daemon is where a declaration meets the real schema.
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
- /* Absent ⇒ the create dialog's gallery, where you go once you know what you want. `create` puts a card on
58
- * the page that makes the automation switched off in one click; `configure` puts one there that opens the
59
- * dialog prefilled, for a template that cannot work unconfigured. Both are for what a user would never
60
- * think to go looking for, and a pack that marked everything as offered would have built a gallery with
61
- * extra steps. */
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 THIS codebase (the chores shelf) rather than the outside world. Declared
69
- // rather than read off the trigger: a nightly dependency sweep and a nightly Stripe poll are both schedules.
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 "+" install dialog renders for a capability's config form (a slug key, a label, secret/optional
7
- // flags, an optional select, a `when` gate). Mirrors the platform catalog's field shape so the web can render
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
- /* The answers most connections never change, folded behind the form's one "Advanced" disclosure. A form's
17
- * length is what it costs to read, and a card that asks six questions whose defaults are right for nearly
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
- // An OPT-IN EXTRA rather than a decision, rendered as a switch, carried as the "on"/"off" the config
27
- // schemas already speak (the vpn's pfs/aggressive precedent). A two-option Segmented can express the same
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, for what the user cannot see from the label alone (a host requirement, when a
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
- /* This field's value only takes effect after the sandbox is REBUILT, it rides the environment overlay
48
- * rather than something the daemon can act on now. Rendered as a chip beside the label.
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
- /* Gates this field on the answers already given, the SSH credential that only applies to the auth mode
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, how a card pins a discriminator
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
- /* This field holds a TOTP seed, the base32 key (or otpauth:// URI) a service shows when enrolling an
88
- * authenticator app. Declare it WITH `secret: true`: the seed is a durable second factor, so it is never
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
- /* WHETHER A FIELD IS IN PLAY, given what has been answered so far, and the only place that decides it.
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
- /* HOW THIS CARD'S SETTINGS ARE TESTED, before they are saved. One authenticated request the daemon makes on the
118
- * form's behalf (capabilities.probe), declared as data for the same reason the form is: the check is per-service
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
- /* WHAT THE SERVICE CALLED THE CALLER, read out of the JSON answer so success can be specific: "authenticated
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 "+" card an entry renders: how it looks in the grid and how the user gets the credential it asks for.
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
- // ONE LINE, aim for 60 characters or fewer. The grid clamps this at two lines and a card sits beside two
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. Shown under the add-form, and searched from the catalog, so the words that identify this
166
- // card to someone hunting for it ("webauthn", "socket mode") belong here even when the tile can't show them.
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 (the platform catalog's guide shape).
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 that becomes the card's /capabilities/<id> route AND the discriminator
188
- // value the daemon's handler resolves (a cli `provider`, a browser/host `platform`), plus the card and its form.
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
- /* A CAPABILITY CARD AS DATA. The catalog is the extensible layer; the HANDLERS are core, so an entry names one
196
- * of the kinds whose daemon-side machinery is fully generic over its data, and the machinery stays put. The
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 AGENT gets, authenticated: the env vars its shell receives (value templates over the
212
- // fields, `${field}` substitutes, `${field:uri}` percent-encodes), a SKILL.md cheatsheet, and an optional
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
- /* PREFER THIS over `fragment` whenever the sandbox already ships a pack for the tool. A pack
233
- * reference is resolved through the image's pack stamps, so on an image that BAKES the pack the
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
- /* A site the agent acts on AS THE OWNER, through the shared logged-in Chromium. `loginUrl` is what the
251
- * sign-in window opens; the profile it persists is the credential. `homeUrl` is where that same profile
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, the skill pack that teaches the agent THAT machine's
288
- // shell. The enrollment, the socket and the scope enforcement are core; only the pack varies.
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
- /* A BROWSER FAMILY the user can connect their own copy of, through the extension they install in it.
295
- * The `host` arm one layer in: the enrollment, the socket, the tool surface and the per-site grants are
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
- /* A PRESET over a core kind: no payload at all, just a named card whose `fields` carry the defaults. What an
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
- // The totp flag's one invariant, enforced where the manifest is parsed rather than trusted to authors: a
315
- // seed the daemon mints codes from must never ride the env into the agent's shell, that would hand the
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
- // The arms carrying a per-instance SKILL.md, the daemon templates and installs these identically.
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
- /* The config key a kind's cards PIN to their own id, so a stored capability can be traced back to the card that
334
- * made it, the daemon resolves the entry's handler data through it, and the web tells one card's instances from
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
@@ -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
- // An optional global keyboard shortcut, in the host's chord notation (`Mod`/`Ctrl`/`Shift`/`Alt` + key, e.g.
11
- // "Mod+Shift+K"; `Mod` = ⌘ on Apple, Ctrl elsewhere). It is DECLARED here so it rides the install dialog's
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
- /* When the KEYBINDING applies, a condition over the shell's context keys (`tabSurface == 'chat'`,
22
- * `!editableTarget`; see @intentic/base/when). The palette ignores it: a command is always runnable by
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" })
@@ -1,16 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import type { ContributionPoint } from "../contribution-point.js";
3
3
 
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,
6
- * see DocumentProviderRegistration.
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 are present at runtime
5
- // (a whisper binary, a psql client, …). The daemon rejects FROM and privileged `# intentic:runtime` directives
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
- /* NO `pack` FIELD HERE, deliberately, though its sibling the `cli` contribution has one. A pack reference is
9
- * the better way to ask for a tool the sandbox already ships (see that field), but expressing "either a
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()