@openbrf/plugin-sdk 0.0.0-stage → 1.2.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 (134) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSE-EXCEPTION.md +28 -0
  3. package/README.md +94 -2
  4. package/dist/abort-signal.d.ts +23 -0
  5. package/dist/abort-signal.d.ts.map +1 -0
  6. package/dist/abort-signal.js +2 -0
  7. package/dist/abort-signal.js.map +1 -0
  8. package/dist/action-refusal.d.ts +101 -0
  9. package/dist/action-refusal.d.ts.map +1 -0
  10. package/dist/action-refusal.js +96 -0
  11. package/dist/action-refusal.js.map +1 -0
  12. package/dist/action-schema.d.ts +42 -0
  13. package/dist/action-schema.d.ts.map +1 -0
  14. package/dist/action-schema.js +67 -0
  15. package/dist/action-schema.js.map +1 -0
  16. package/dist/actions.d.ts +287 -0
  17. package/dist/actions.d.ts.map +1 -0
  18. package/dist/actions.js +68 -0
  19. package/dist/actions.js.map +1 -0
  20. package/dist/api-version.d.ts +23 -0
  21. package/dist/api-version.d.ts.map +1 -0
  22. package/dist/api-version.js +27 -0
  23. package/dist/api-version.js.map +1 -0
  24. package/dist/blocks.d.ts +310 -0
  25. package/dist/blocks.d.ts.map +1 -0
  26. package/dist/blocks.js +185 -0
  27. package/dist/blocks.js.map +1 -0
  28. package/dist/catalog.d.ts +707 -0
  29. package/dist/catalog.d.ts.map +1 -0
  30. package/dist/catalog.js +567 -0
  31. package/dist/catalog.js.map +1 -0
  32. package/dist/define-plugin.d.ts +14 -0
  33. package/dist/define-plugin.d.ts.map +1 -0
  34. package/dist/define-plugin.js +15 -0
  35. package/dist/define-plugin.js.map +1 -0
  36. package/dist/entitlements.d.ts +117 -0
  37. package/dist/entitlements.d.ts.map +1 -0
  38. package/dist/entitlements.js +62 -0
  39. package/dist/entitlements.js.map +1 -0
  40. package/dist/extensions.d.ts +183 -0
  41. package/dist/extensions.d.ts.map +1 -0
  42. package/dist/extensions.js +67 -0
  43. package/dist/extensions.js.map +1 -0
  44. package/dist/finances.d.ts +294 -0
  45. package/dist/finances.d.ts.map +1 -0
  46. package/dist/finances.js +56 -0
  47. package/dist/finances.js.map +1 -0
  48. package/dist/findings.d.ts +156 -0
  49. package/dist/findings.d.ts.map +1 -0
  50. package/dist/findings.js +146 -0
  51. package/dist/findings.js.map +1 -0
  52. package/dist/host.d.ts +827 -0
  53. package/dist/host.d.ts.map +1 -0
  54. package/dist/host.js +80 -0
  55. package/dist/host.js.map +1 -0
  56. package/dist/imports.d.ts +104 -0
  57. package/dist/imports.d.ts.map +1 -0
  58. package/dist/imports.js +70 -0
  59. package/dist/imports.js.map +1 -0
  60. package/dist/index.d.ts +34 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +31 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/integrity.d.ts +38 -0
  65. package/dist/integrity.d.ts.map +1 -0
  66. package/dist/integrity.js +105 -0
  67. package/dist/integrity.js.map +1 -0
  68. package/dist/localized-text.d.ts +18 -0
  69. package/dist/localized-text.d.ts.map +1 -0
  70. package/dist/localized-text.js +17 -0
  71. package/dist/localized-text.js.map +1 -0
  72. package/dist/manifest.d.ts +1122 -0
  73. package/dist/manifest.d.ts.map +1 -0
  74. package/dist/manifest.js +1118 -0
  75. package/dist/manifest.js.map +1 -0
  76. package/dist/module.d.ts +35 -0
  77. package/dist/module.d.ts.map +1 -0
  78. package/dist/module.js +2 -0
  79. package/dist/module.js.map +1 -0
  80. package/dist/package-check.d.ts +66 -0
  81. package/dist/package-check.d.ts.map +1 -0
  82. package/dist/package-check.js +755 -0
  83. package/dist/package-check.js.map +1 -0
  84. package/dist/permissions.d.ts +224 -0
  85. package/dist/permissions.d.ts.map +1 -0
  86. package/dist/permissions.js +277 -0
  87. package/dist/permissions.js.map +1 -0
  88. package/dist/recipients.d.ts +139 -0
  89. package/dist/recipients.d.ts.map +1 -0
  90. package/dist/recipients.js +223 -0
  91. package/dist/recipients.js.map +1 -0
  92. package/dist/records.d.ts +130 -0
  93. package/dist/records.d.ts.map +1 -0
  94. package/dist/records.js +85 -0
  95. package/dist/records.js.map +1 -0
  96. package/dist/require-capability.d.ts +37 -0
  97. package/dist/require-capability.d.ts.map +1 -0
  98. package/dist/require-capability.js +88 -0
  99. package/dist/require-capability.js.map +1 -0
  100. package/dist/settings-schema.d.ts +130 -0
  101. package/dist/settings-schema.d.ts.map +1 -0
  102. package/dist/settings-schema.js +272 -0
  103. package/dist/settings-schema.js.map +1 -0
  104. package/dist/standard-schema.d.ts +43 -0
  105. package/dist/standard-schema.d.ts.map +1 -0
  106. package/dist/standard-schema.js +2 -0
  107. package/dist/standard-schema.js.map +1 -0
  108. package/package.json +55 -3
  109. package/src/abort-signal.ts +22 -0
  110. package/src/action-refusal.ts +178 -0
  111. package/src/action-schema.ts +89 -0
  112. package/src/actions.ts +344 -0
  113. package/src/api-version.ts +28 -0
  114. package/src/blocks.ts +289 -0
  115. package/src/catalog.ts +679 -0
  116. package/src/define-plugin.ts +18 -0
  117. package/src/entitlements.ts +150 -0
  118. package/src/extensions.ts +212 -0
  119. package/src/finances.ts +317 -0
  120. package/src/findings.ts +163 -0
  121. package/src/host.ts +894 -0
  122. package/src/imports.ts +135 -0
  123. package/src/index.ts +316 -0
  124. package/src/integrity.ts +125 -0
  125. package/src/localized-text.ts +19 -0
  126. package/src/manifest.ts +1412 -0
  127. package/src/module.ts +38 -0
  128. package/src/package-check.ts +894 -0
  129. package/src/permissions.ts +295 -0
  130. package/src/recipients.ts +294 -0
  131. package/src/records.ts +172 -0
  132. package/src/require-capability.ts +106 -0
  133. package/src/settings-schema.ts +334 -0
  134. package/src/standard-schema.ts +39 -0
@@ -0,0 +1,178 @@
1
+ import type { ActionRetry } from "./actions.ts";
2
+
3
+ /**
4
+ * Why the registry refused a call, in a form a caller can act on.
5
+ *
6
+ * A caller here is often a model rather than a person, which changes what a
7
+ * refusal has to carry. A person reads a sentence and decides what to do; a
8
+ * model needs to know whether trying again could ever work, and what to change
9
+ * if it could. So every refusal has a code, a status, and a published rule for
10
+ * how a connector should render it.
11
+ *
12
+ * The vocabulary lives here rather than in the API because a plugin dispatching
13
+ * through `host.actions.invoke` has no other way to read it: the catalogue
14
+ * endpoint serves the same table over HTTP, and a plugin in the same process
15
+ * has no route to that. The API imports it back, so there is one list.
16
+ */
17
+
18
+ export const ACTION_ERROR_REASONS = [
19
+ "unknown-action",
20
+ "forbidden-capability",
21
+ "forbidden-surface",
22
+ "insufficient-scope",
23
+ "not-serving",
24
+ "read-only",
25
+ "confirmation-required",
26
+ "output-invalid",
27
+ "rate-limited",
28
+ "internal",
29
+ ] as const;
30
+
31
+ export type ActionErrorReason = (typeof ACTION_ERROR_REASONS)[number];
32
+
33
+ /**
34
+ * How a connector should tell its caller about a refusal.
35
+ *
36
+ * A protocol error means the call never happened; a tool result means it
37
+ * happened and was refused, and only the second is something to tell the
38
+ * person about.
39
+ */
40
+ export type ActionRefusalTransport = "protocol" | "tool-result";
41
+
42
+ /**
43
+ * How a connector should render each refusal, published as data.
44
+ *
45
+ * Served on the catalogue endpoint as well, so that two connectors - one
46
+ * reading it over HTTP, one in process - cannot disagree about whether a 403
47
+ * is worth retrying. Keyed by the registry's reasons and by the two refusals
48
+ * that are not the registry's own: a body the action's input schema refuses,
49
+ * and a refusal raised by the service behind the action.
50
+ */
51
+ export const ACTION_ERROR_MCP: Readonly<
52
+ Record<
53
+ ActionErrorReason | "invalid-body" | "domain",
54
+ {
55
+ readonly transport: ActionRefusalTransport;
56
+ readonly retry: ActionRetry;
57
+ }
58
+ >
59
+ > = {
60
+ "unknown-action": { transport: "protocol", retry: "never" },
61
+ "invalid-body": { transport: "tool-result", retry: "after-edit" },
62
+ "forbidden-capability": { transport: "tool-result", retry: "never" },
63
+ "forbidden-surface": { transport: "tool-result", retry: "never" },
64
+ "insufficient-scope": { transport: "tool-result", retry: "never" },
65
+ "not-serving": { transport: "tool-result", retry: "never" },
66
+ "read-only": { transport: "tool-result", retry: "after-backoff" },
67
+ "confirmation-required": { transport: "tool-result", retry: "never" },
68
+ "output-invalid": { transport: "protocol", retry: "never" },
69
+ "rate-limited": { transport: "tool-result", retry: "after-backoff" },
70
+ internal: { transport: "protocol", retry: "after-backoff" },
71
+ domain: { transport: "tool-result", retry: "never" },
72
+ };
73
+
74
+ /**
75
+ * Which of the three ways a call was refused.
76
+ *
77
+ * "registry" is the registry's own refusal, and `reason` is then one of
78
+ * ACTION_ERROR_REASONS. "invalid-body" is an input the action's schema
79
+ * refused, and `reason` is "invalid-body". "domain" is the service behind the
80
+ * action refusing on the merits, and `reason` is one the action declared in
81
+ * its `errors`.
82
+ */
83
+ export type ActionRefusalKind = "registry" | "invalid-body" | "domain";
84
+
85
+ /**
86
+ * What `host.actions.invoke` rejects with when the call was refused.
87
+ *
88
+ * One shape for all three kinds, so that a connector renders a refusal from
89
+ * the fields it carries rather than by recognising the host's classes, which a
90
+ * plugin cannot import. Recognise one with `isActionRefusal`, not with
91
+ * `instanceof`: the host builds it, and a plugin's bundled copy of this
92
+ * package is not the host's.
93
+ *
94
+ * A plugin route that rethrows one answers its caller with the same status,
95
+ * body and headers the registry would have.
96
+ */
97
+ export interface ActionRefusal extends Error {
98
+ readonly name: "ActionRefusal";
99
+ readonly kind: ActionRefusalKind;
100
+ readonly reason: string;
101
+ /** The HTTP status the refusal answers with. */
102
+ readonly status: number;
103
+ readonly transport: ActionRefusalTransport;
104
+ /**
105
+ * From ACTION_ERROR_MCP for the registry's refusals and an invalid body. For
106
+ * a domain refusal, the verdict the action declared for that reason in its
107
+ * `errors`, or "never" for a reason it did not declare.
108
+ */
109
+ readonly retry: ActionRetry;
110
+ /**
111
+ * The particulars the refusal publishes, keyed by the field they travel in.
112
+ *
113
+ * `scopes` on an insufficient-scope refusal names the scope the call needed.
114
+ * `issues` on an invalid body lists `{ path, message }` for each failing
115
+ * field, and never the submitted value.
116
+ */
117
+ details(): Record<string, readonly unknown[]>;
118
+ /**
119
+ * The bearer challenge to answer a connected app with, as the value of a
120
+ * `WWW-Authenticate` header (RFC 6750, ADR 0009).
121
+ *
122
+ * Set on an insufficient-scope refusal, where it names every scope the
123
+ * instance issues so the client can ask for what it is missing in one round
124
+ * trip. Absent wherever no challenge would help.
125
+ */
126
+ readonly challenge?: string;
127
+ }
128
+
129
+ const KINDS: readonly unknown[] = [
130
+ "registry",
131
+ "invalid-body",
132
+ "domain",
133
+ ] satisfies ActionRefusalKind[];
134
+ const TRANSPORTS: readonly unknown[] = [
135
+ "protocol",
136
+ "tool-result",
137
+ ] satisfies ActionRefusalTransport[];
138
+ const RETRIES: readonly unknown[] = [
139
+ "never",
140
+ "after-edit",
141
+ "after-backoff",
142
+ ] satisfies ActionRetry[];
143
+ const REGISTRY_REASONS: readonly unknown[] = ACTION_ERROR_REASONS;
144
+
145
+ /**
146
+ * Whether a value `host.actions.invoke` rejected with is a refusal.
147
+ *
148
+ * Structural rather than `instanceof`, for the reason on ActionRefusal. False
149
+ * means the call failed rather than being refused - the host could not be
150
+ * reached, or something broke - and a connector answers that as an internal
151
+ * error without inspecting it.
152
+ */
153
+ export function isActionRefusal(value: unknown): value is ActionRefusal {
154
+ if (!(value instanceof Error) || value.name !== "ActionRefusal") {
155
+ return false;
156
+ }
157
+ const refusal = value as Error &
158
+ Partial<Record<keyof ActionRefusal, unknown>>;
159
+ if (
160
+ typeof refusal.reason !== "string" ||
161
+ typeof refusal.status !== "number" ||
162
+ typeof refusal.details !== "function" ||
163
+ !KINDS.includes(refusal.kind) ||
164
+ !TRANSPORTS.includes(refusal.transport) ||
165
+ !RETRIES.includes(refusal.retry) ||
166
+ (refusal.challenge !== undefined && typeof refusal.challenge !== "string")
167
+ ) {
168
+ return false;
169
+ }
170
+ switch (refusal.kind) {
171
+ case "registry":
172
+ return REGISTRY_REASONS.includes(refusal.reason);
173
+ case "invalid-body":
174
+ return refusal.reason === "invalid-body";
175
+ default:
176
+ return true;
177
+ }
178
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+
3
+ import type { StandardSchemaV1 } from "./standard-schema.ts";
4
+
5
+ /**
6
+ * An action's schemas, as the JSON Schema a caller is published.
7
+ *
8
+ * The only call to `z.toJSONSchema` in the product. Every option here is set
9
+ * against a failure that would otherwise be silent, so they are worth reading
10
+ * one at a time.
11
+ *
12
+ * `io: "input"` because output mode makes a key with a default REQUIRED, and a
13
+ * model reading that will dutifully supply a value for every field the platform
14
+ * was perfectly happy to choose itself.
15
+ *
16
+ * `cycles: "throw"` because a self-referential schema has no JSON Schema
17
+ * without `$ref`, and a silently emitted `$ref` to a positional name is worse
18
+ * than a refusal at registration.
19
+ *
20
+ * `reused: "inline"` because `$defs` invents names from the position a schema
21
+ * happens to hold, and those names would reach a committed artefact and a
22
+ * third-party client. Inlining costs bytes and keeps the document stable.
23
+ *
24
+ * The function form of `unrepresentable` because the default error names
25
+ * neither the action nor the path, and a build failing with "unrepresentable
26
+ * type" and nothing else is a half hour of searching.
27
+ *
28
+ * One property is worth stating because it is NOT what one would assume: a bare
29
+ * `.transform()` does not throw under input mode - it is erased and the
30
+ * pre-transform type is emitted. The guarantee is placed where it is real, on
31
+ * the output helper, which does throw. So an action whose pair is converted at
32
+ * registration cannot carry a transform anywhere.
33
+ *
34
+ * `.refine()`, `.check()`, `.superRefine()` and `.brand()` are erased with no
35
+ * marker under both modes. That is why the rule for a published schema is
36
+ * structural constraints first: what the document says has to be at least as
37
+ * strict as what the runtime enforces, or a caller is told a value is
38
+ * acceptable that the service will refuse.
39
+ */
40
+
41
+ /** Refuses a schema built with a second copy of zod, whose internals differ. */
42
+ export function isHostZodSchema(schema: unknown): schema is z.ZodType {
43
+ return (
44
+ typeof schema === "object" &&
45
+ schema !== null &&
46
+ "_zod" in schema &&
47
+ (schema as { _zod?: unknown })._zod !== undefined
48
+ );
49
+ }
50
+
51
+ function convert(
52
+ schema: unknown,
53
+ actionName: string,
54
+ io: "input" | "output",
55
+ ): Record<string, unknown> {
56
+ if (!isHostZodSchema(schema)) {
57
+ throw new Error(
58
+ `${actionName}: an action schema must be built with the host's zod.`,
59
+ );
60
+ }
61
+ const document = z.toJSONSchema(schema, {
62
+ target: "draft-2020-12",
63
+ io,
64
+ cycles: "throw",
65
+ reused: "inline",
66
+ unrepresentable: ({ path, message }) => {
67
+ throw new Error(
68
+ `${actionName}: ${message} at ${[io, ...path.map(String)].join(".")}`,
69
+ );
70
+ },
71
+ }) as Record<string, unknown>;
72
+ // zod hangs a non-enumerable `~standard` on the root. A shallow copy leaves
73
+ // it behind, so what a caller is handed is a plain JSON document.
74
+ return { ...document };
75
+ }
76
+
77
+ export function actionInputJsonSchema(
78
+ schema: StandardSchemaV1<unknown, unknown>,
79
+ actionName: string,
80
+ ): Record<string, unknown> {
81
+ return convert(schema, actionName, "input");
82
+ }
83
+
84
+ export function actionOutputJsonSchema(
85
+ schema: StandardSchemaV1<unknown, unknown>,
86
+ actionName: string,
87
+ ): Record<string, unknown> {
88
+ return convert(schema, actionName, "output");
89
+ }
package/src/actions.ts ADDED
@@ -0,0 +1,344 @@
1
+ import type { StandardSchemaV1 } from "./standard-schema.ts";
2
+
3
+ /**
4
+ * What the platform can be asked to do, declared once and dispatched through
5
+ * one place: an action (atgard).
6
+ *
7
+ * An action is a name, an input schema, the single capability it needs, what it
8
+ * does to the records, which categories of personal data it can touch, and which
9
+ * surfaces it may be offered on. The registry checks the calling person's
10
+ * capability on every call rather than trusting the caller, so registering an
11
+ * action is never a permission of its own - it is a way of reaching something
12
+ * the caller could already do.
13
+ *
14
+ * A core feature registers its own. A plugin registers the ones its manifest
15
+ * declared and the board consented to, and they disappear with the plugin.
16
+ *
17
+ * Swedish domain terms follow GLOSSARY.md.
18
+ */
19
+
20
+ export const ACTION_EFFECTS = ["read", "write", "delete"] as const;
21
+ export type ActionEffect = (typeof ACTION_EFFECTS)[number];
22
+
23
+ /**
24
+ * Where an action may be offered.
25
+ *
26
+ * "ui" means in-process only: the platform's own screens and a plugin's own
27
+ * route. The other two are the ones a board switches on deliberately.
28
+ */
29
+ export const ACTION_SURFACES = ["ui", "mcp", "ai"] as const;
30
+ export type ActionSurface = (typeof ACTION_SURFACES)[number];
31
+
32
+ /**
33
+ * What an action can touch.
34
+ *
35
+ * The first thirteen are PERSONAL_DATA_CATEGORIES from @openbrf/shared, in
36
+ * order, which this package deliberately does not depend on; the API side
37
+ * asserts the prefix equality, because it is the only place that can import
38
+ * both. "protected" is this list's own and marks an action that can return a
39
+ * field of a person carrying protected personal data (skyddade
40
+ * personuppgifter): it is the value the registry refuses to expose to a
41
+ * connected app or to the AI package at all.
42
+ */
43
+ export const ACTION_PERSONAL_DATA = [
44
+ "name",
45
+ "apartment",
46
+ "residency",
47
+ "email",
48
+ "phone",
49
+ "postalAddress",
50
+ "personalIdentityNumber",
51
+ "account",
52
+ "financial",
53
+ "health",
54
+ "photograph",
55
+ "freeText",
56
+ "auditTrail",
57
+ "protected",
58
+ ] as const;
59
+ export type ActionPersonalData = (typeof ACTION_PERSONAL_DATA)[number];
60
+
61
+ /**
62
+ * What a public action name may look like.
63
+ *
64
+ * The intersection of three separate limits: what an MCP tools/list entry
65
+ * allows, what a model API allows (no dots), and what stays readable in a
66
+ * directory review (64 characters).
67
+ */
68
+ export const ACTION_NAME_PATTERN = /^[a-z][a-z0-9_]{2,63}$/;
69
+
70
+ /** Whether trying again could ever work, and after what. */
71
+ export type ActionRetry = "never" | "after-edit" | "after-backoff";
72
+
73
+ /** What a refusal means for a caller that is a model rather than a person. */
74
+ export interface ActionErrorSpec {
75
+ /** The machine-readable reason the handler raises. */
76
+ readonly reason: string;
77
+ /** Whether trying again could ever work, and after what. */
78
+ readonly retry: ActionRetry;
79
+ /** The sentence a person is shown, as an i18n key. */
80
+ readonly messageKey: string;
81
+ }
82
+
83
+ /**
84
+ * The request a plugin's own route received.
85
+ *
86
+ * Opaque on purpose: the host recognises the request it authenticated by its
87
+ * identity, which nothing on it can copy, so a plugin passes the request it was
88
+ * handed straight through and cannot construct one. That is what stops a plugin
89
+ * naming a person it was not given.
90
+ *
91
+ * It speaks for its person only while the route is serving it. Once the
92
+ * response is sent the host refuses it, so a request kept for later names
93
+ * nobody.
94
+ */
95
+ export type ActionRequest = object;
96
+
97
+ /** The lowercase channel vocabulary. The registry maps it to the audit channel. */
98
+ export const ACTION_CHANNELS = [
99
+ "web",
100
+ "mcp",
101
+ "ai",
102
+ "system",
103
+ "plugin",
104
+ ] as const;
105
+ export type ActionChannel = (typeof ACTION_CHANNELS)[number];
106
+
107
+ /**
108
+ * Who is acting.
109
+ *
110
+ * The person, and the capabilities the registry re-derived for this call. No
111
+ * email, no name, no roles: a handler that needs to decide what an audience may
112
+ * see reads the capability list, and a handler that does not need it is not
113
+ * given a person's details to leak.
114
+ */
115
+ export interface ActionPrincipal {
116
+ readonly personId: string;
117
+ readonly capabilities: readonly string[];
118
+ }
119
+
120
+ /** The connected app that acted, when one did. Both hosts, never the token. */
121
+ export interface ActionClient {
122
+ readonly clientId: string;
123
+ readonly clientHost: string | null;
124
+ }
125
+
126
+ /** What a handler is told about the call. Built by the host, never by a caller. */
127
+ export interface ActionContext {
128
+ readonly principal: ActionPrincipal;
129
+ readonly channel: ActionChannel;
130
+ readonly client?: ActionClient;
131
+ /**
132
+ * The plugin whose route dispatched the call, when one did, by its manifest
133
+ * id. Set by the host from the plugin it serves, whatever the channel.
134
+ */
135
+ readonly pluginId?: string;
136
+ /**
137
+ * The pending change this call confirms, when it confirms one.
138
+ *
139
+ * A confirmed call is the confirming person's own, on `web`; this is what
140
+ * ties its audit entry back to the proposal, and to the channel and the app
141
+ * the proposal came through.
142
+ */
143
+ readonly pendingChangeId?: string;
144
+ readonly requestId: string;
145
+ readonly now: Date;
146
+ }
147
+
148
+ export interface ActionDefinition<Input = unknown, Output = unknown> {
149
+ readonly name: string;
150
+ readonly titleKey: string;
151
+ readonly descriptionKey: string;
152
+ readonly group: string;
153
+ readonly groupTitleKey: string;
154
+ /** Exactly one core capability, by name. */
155
+ readonly capability: string;
156
+ readonly effect: ActionEffect;
157
+ readonly idempotent: boolean;
158
+ /**
159
+ * True when the act adds and overwrites nothing a person wrote.
160
+ *
161
+ * It is what separates a create from an update, and the annotation mapping
162
+ * reads it: MCP's destructiveHint defaults to true, so a non-read action has
163
+ * to say for itself that it destroys nothing.
164
+ */
165
+ readonly additive: boolean;
166
+ readonly needsConfirmation: boolean;
167
+ /** True when the handler reaches a system outside this instance. */
168
+ readonly openWorld: boolean;
169
+ /** Never empty by convention: an action that touches none says []. */
170
+ readonly personalData: readonly ActionPersonalData[];
171
+ /** Opt-in allowlist. An empty array means the action is offered nowhere. */
172
+ readonly surfaces: readonly ActionSurface[];
173
+ /** Every domain refusal the handler can raise, for the caller to act on. */
174
+ readonly errors: readonly ActionErrorSpec[];
175
+ readonly deprecatedAliases?: readonly string[];
176
+ /** Every object in it, at every depth, must refuse unknown keys. */
177
+ readonly input: StandardSchemaV1<unknown, Input>;
178
+ readonly output: StandardSchemaV1<unknown, Output>;
179
+ readonly handler: (input: Input, context: ActionContext) => Promise<Output>;
180
+ }
181
+
182
+ /**
183
+ * What a plugin hands the host: the id its manifest declared, and the rest.
184
+ *
185
+ * The capability, the effect, the personal-data categories and the surfaces are
186
+ * manifest data and are not repeated here. The loader's rule is that nothing
187
+ * which can refuse a plugin may run after the code that executes it, and the
188
+ * install consent screen renders from the catalog before anything is
189
+ * downloaded - so what the board consents to cannot come from the bundle.
190
+ */
191
+ export interface PluginActionRegistration {
192
+ readonly id: string;
193
+ readonly definition: Omit<
194
+ ActionDefinition,
195
+ "capability" | "effect" | "personalData" | "surfaces"
196
+ >;
197
+ }
198
+
199
+ /** Narrows what a plugin's route offers the request it is serving. */
200
+ export interface PluginActionFilter {
201
+ readonly surface?: ActionSurface;
202
+ readonly group?: string;
203
+ }
204
+
205
+ /** One action as a caller sees it, with its text already in the right language. */
206
+ export interface ActionSummary {
207
+ readonly name: string;
208
+ readonly title: string;
209
+ readonly description: string;
210
+ readonly titleKey: string;
211
+ readonly descriptionKey: string;
212
+ readonly group: string;
213
+ readonly groupTitle: string;
214
+ /**
215
+ * The key behind `groupTitle`, alongside the two above.
216
+ *
217
+ * Both forms travel for the reason `titleKey` does: the resolved string is
218
+ * for a caller with no translations of its own - a connected app, a model -
219
+ * while a screen in this product translates the key itself. The interface's
220
+ * language and the language a request happens to declare are not the same
221
+ * thing, so a screen rendering the resolved string can show one heading in
222
+ * the browser's language above a page in the association's.
223
+ */
224
+ readonly groupTitleKey: string;
225
+ readonly capability: string;
226
+ readonly effect: ActionEffect;
227
+ readonly idempotent: boolean;
228
+ readonly additive: boolean;
229
+ readonly needsConfirmation: boolean;
230
+ readonly openWorld: boolean;
231
+ readonly personalData: readonly ActionPersonalData[];
232
+ readonly surfaces: readonly ActionSurface[];
233
+ readonly errors: readonly ActionErrorSpec[];
234
+ }
235
+
236
+ /**
237
+ * What proposing a change answers with.
238
+ *
239
+ * Nothing has run. The change waits, for a week at most, until the person whose
240
+ * request proposed it confirms or rejects it on the platform's own screen at
241
+ * `path`, which a plugin's view links to. A plugin cannot confirm one.
242
+ */
243
+ export interface PendingChangeReceipt {
244
+ readonly id: string;
245
+ readonly expiresAt: Date;
246
+ /** The preview, on the platform's own screen, relative to the origin. */
247
+ readonly path: string;
248
+ }
249
+
250
+ /**
251
+ * One action as a plugin is offered it: the summary, and the JSON Schema
252
+ * (draft 2020-12) of what it takes and what it returns.
253
+ *
254
+ * What an MCP connector puts in `tools/list` and what a model is told a tool
255
+ * accepts. The input schema is the one `invoke` validates against, converted
256
+ * as the HTTP catalogue converts it, so a tool is described by the rule it is
257
+ * held to. Each listing builds both afresh: changing one changes nothing the
258
+ * host holds or hands anybody else.
259
+ */
260
+ export interface ActionWithSchemas extends ActionSummary {
261
+ readonly inputSchema: Record<string, unknown>;
262
+ readonly outputSchema: Record<string, unknown>;
263
+ }
264
+
265
+ export interface PluginActions {
266
+ /**
267
+ * Registers an action the manifest declared.
268
+ *
269
+ * Callable from the module factory onwards: a call made that early is
270
+ * buffered until the application is built, because the host object is not
271
+ * bound yet.
272
+ *
273
+ * It does not throw for a registration the host will refuse. An id the
274
+ * manifest never declared, or a schema that cannot be published, is found
275
+ * when the buffer is flushed - long after this call returned - and reaches
276
+ * the board as an `action-refused` finding rather than as an error the
277
+ * factory could have caught. A plugin that wants to know sooner validates
278
+ * its own manifest in its own build.
279
+ */
280
+ register: (registration: PluginActionRegistration) => void;
281
+
282
+ /**
283
+ * What this plugin's route may offer the request it is serving, each with
284
+ * its schemas.
285
+ *
286
+ * Filtered by channel, surface, capability, liveness and arming, as `invoke`
287
+ * is, and the schemas are of the actions listed and no others. Token scope
288
+ * (`mcp:read`, `mcp:write`) and read-only mode are decided at `invoke`, which
289
+ * may still refuse a listed action.
290
+ */
291
+ list: (
292
+ request: ActionRequest,
293
+ filter?: PluginActionFilter,
294
+ ) => Promise<ActionWithSchemas[]>;
295
+
296
+ /**
297
+ * Dispatch. The person, the channel and the connected app all come from the
298
+ * request the host authenticated, never from an argument.
299
+ *
300
+ * A refused call rejects with an `ActionRefusal`, whichever of the three
301
+ * ways it was refused - by the registry, over its input or by the service
302
+ * behind it - and `isActionRefusal` recognises one. Anything else it
303
+ * rejects with is a fault rather than an answer.
304
+ */
305
+ invoke: <Output>(
306
+ request: ActionRequest,
307
+ name: string,
308
+ input: unknown,
309
+ ) => Promise<Output>;
310
+
311
+ /**
312
+ * Holds a write as a pending change instead of running it (ADR 0039).
313
+ *
314
+ * The call is checked as `invoke` would check it before running anything -
315
+ * the action exists and is offered, the person holds its capability, the
316
+ * input fits its schema - and refused the same way. A read cannot be
317
+ * proposed, and nor can an action the platform's own screens are not offered.
318
+ * The person whose request this is then sees the change as a preview and
319
+ * confirms it, and it runs as them at that moment, with every check made
320
+ * again; or rejects it, and nothing runs.
321
+ *
322
+ * Added after `invoke`, so a plugin installed on an older host has to check
323
+ * that it is there before calling it, for the reason `host.actions` gives.
324
+ */
325
+ propose: (
326
+ request: ActionRequest,
327
+ name: string,
328
+ input: unknown,
329
+ ) => Promise<PendingChangeReceipt>;
330
+
331
+ /**
332
+ * The connected app the request was authenticated with, or null when it
333
+ * arrived on a signed-in session.
334
+ *
335
+ * The app a dispatch on this request is recorded as, read from what the host
336
+ * established when it authenticated the request rather than from the
337
+ * request's own properties, which the plugin can write. A connector asks it
338
+ * before answering on a route that serves connected apps only.
339
+ *
340
+ * Added after `list` and `invoke`, so a plugin must feature-detect it
341
+ * (`host.actions?.client === undefined`) before calling it.
342
+ */
343
+ client: (request: ActionRequest) => ActionClient | null;
344
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The plugin API version.
3
+ *
4
+ * A single integer rather than a semver range, because the gate it guards is
5
+ * binary: a plugin either speaks the contract this host implements or it does
6
+ * not get loaded. A range would invite a plugin to claim ">=1" and then fail
7
+ * at the first call to a method version 2 removed, which is exactly the class
8
+ * of breakage the gate exists to prevent.
9
+ *
10
+ * Additive changes - a new optional manifest field, a new SDK method - keep
11
+ * the number. Anything a plugin built against the old contract could notice
12
+ * raises it, and the host may then support more than one version at a time by
13
+ * listing them in SUPPORTED_PLUGIN_API_VERSIONS.
14
+ */
15
+ export const PLUGIN_API_VERSION = 1;
16
+
17
+ /**
18
+ * Every version this contract can still load. Kept as a list so a host can
19
+ * carry an old version through a deprecation window without a plugin having
20
+ * to be republished on the day the new one lands.
21
+ */
22
+ export const SUPPORTED_PLUGIN_API_VERSIONS: readonly number[] = [
23
+ PLUGIN_API_VERSION,
24
+ ];
25
+
26
+ export function isSupportedApiVersion(apiVersion: number): boolean {
27
+ return SUPPORTED_PLUGIN_API_VERSIONS.includes(apiVersion);
28
+ }