@axiom-lattice/protocols 4.7.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/protocols",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "Unified protocol type definitions for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -41,6 +41,12 @@ interface BaseAgentConfig {
41
41
  name: string; // Name
42
42
  description: string; // Description
43
43
  prompt: string; // Prompt
44
+ /**
45
+ * Owner user id when this config represents a user's personal assistant.
46
+ * `undefined` means a tenant/workspace-shared agent, which is the only kind
47
+ * discoverable in shared contexts (e.g. a public room's executor list).
48
+ */
49
+ ownerUserId?: string;
44
50
  /**
45
51
  * Key of the parent agent to inherit configuration from.
46
52
  * When set, unspecified fields are inherited from the parent agent's config.
@@ -112,6 +112,10 @@ export interface ChannelAdapter<TConfig = unknown> {
112
112
  * 可选:断开持久连接并停止接收事件。
113
113
  * 当 channel installation 被禁用或删除时调用。
114
114
  * `installationId` 用于标识要断开的具体安装实例。
115
+ *
116
+ * 实现必须是**幂等**且对未连接的 installation 安全(no-op):多副本下
117
+ * `ChannelConnectionLeader` 在失去租约时会对共享 store 中的全部启用安装
118
+ * 调用 `disconnect`,其中可能包含本副本从未 `connect` 的实例。
115
119
  */
116
120
  disconnect?(installationId: string): Promise<void>;
117
121
  }
@@ -15,15 +15,51 @@ export interface ConnectionEntry {
15
15
  name: string;
16
16
  description?: string;
17
17
  config: Record<string, unknown>;
18
+ /**
19
+ * Names of config fields that hold secrets (API keys, passwords, tokens).
20
+ *
21
+ * Stores persist this list alongside the entry and use it to encrypt those
22
+ * config values at rest on write and transparently decrypt them on read.
23
+ * `config` in a returned entry is always the decrypted, real value — callers
24
+ * (gateway API, runtime plugin resolution) never see ciphertext.
25
+ *
26
+ * Absent/empty means the entry has no encrypted fields (e.g. legacy rows or
27
+ * connection types whose schema defines no password fields).
28
+ */
29
+ secretFields?: string[];
18
30
  createdAt: string;
19
31
  updatedAt: string;
20
32
  }
21
33
 
22
34
  export interface ConnectionStore {
23
35
  listByTenant(tenantId: string): Promise<ConnectionEntry[]>;
36
+ /**
37
+ * List a tenant's connections of one plugin type.
38
+ *
39
+ * `config` in the returned entries is decrypted — implementations decrypt the
40
+ * fields named by each entry's `secretFields` before returning.
41
+ */
24
42
  listByType(tenantId: string, type: string): Promise<ConnectionEntry[]>;
43
+ /**
44
+ * Read one connection by key.
45
+ *
46
+ * `config` in the returned entry is decrypted (see `listByType`).
47
+ */
25
48
  getByKey(tenantId: string, type: string, key: string): Promise<ConnectionEntry | null>;
49
+ /**
50
+ * Create a connection.
51
+ *
52
+ * Implementations encrypt the `secretFields` of `config` at rest; the
53
+ * returned entry carries the plaintext `config` the caller supplied.
54
+ */
26
55
  create(entry: Omit<ConnectionEntry, "id" | "createdAt" | "updatedAt">): Promise<ConnectionEntry>;
56
+ /**
57
+ * Update a connection.
58
+ *
59
+ * When `updates.config` is present, the effective secret fields are
60
+ * `updates.secretFields ?? existing.secretFields`; those fields are
61
+ * re-encrypted before persistence.
62
+ */
27
63
  update(tenantId: string, type: string, key: string, updates: Partial<ConnectionEntry>): Promise<ConnectionEntry | null>;
28
64
  delete(tenantId: string, type: string, key: string): Promise<boolean>;
29
65
  }
@@ -0,0 +1,56 @@
1
+ /** Lifecycle states of a published plugin's resident service. */
2
+ export type PluginDeploymentStatus =
3
+ | "requested"
4
+ | "provisioning"
5
+ | "running"
6
+ | "paused"
7
+ | "stopping"
8
+ | "stopped"
9
+ | "failed";
10
+
11
+ /** Lifecycle strategy: lazily paused vs. permanently resident. */
12
+ export type PluginDeploymentMode = "onDemand" | "alwaysOn";
13
+
14
+ /** Durable tenant-scoped record of one published plugin's resident service. */
15
+ export interface PluginDeployment {
16
+ id: string;
17
+ tenantId: string;
18
+ pluginType: string;
19
+ pluginVersion: string;
20
+ mode: PluginDeploymentMode;
21
+ status: PluginDeploymentStatus;
22
+ /** Backend provider key, e.g. "microsandbox-remote" | "opensandbox". */
23
+ provider: string;
24
+ /** Deterministic sandbox identity (`plugin-<hash(tenantId, pluginType)>`). */
25
+ sandboxName: string;
26
+ /** Guest port published by the host server. */
27
+ port: number;
28
+ /** Resolved endpoint (scheme-less route or host:port); may change over time. */
29
+ endpoint?: string;
30
+ /** Image used to create the sandbox, when pinned. */
31
+ imageRef?: string;
32
+ /** Idle timeout before an onDemand deployment is paused. */
33
+ idleTimeoutMs: number;
34
+ lastHealthAt?: string;
35
+ lastError?: string;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }
39
+
40
+ /** Persistence operations for plugin deployments. */
41
+ export interface PluginDeploymentStore {
42
+ /** Lists every deployment for a tenant. */
43
+ listByTenant(tenantId: string): Promise<PluginDeployment[]>;
44
+ /** Returns the deployment for a plugin type, or undefined. */
45
+ get(tenantId: string, pluginType: string): Promise<PluginDeployment | undefined>;
46
+ /** Inserts or replaces a deployment keyed by tenant + plugin type. */
47
+ upsert(deployment: PluginDeployment): Promise<PluginDeployment>;
48
+ /** Applies a partial update (status/endpoint/health); returns the updated record. */
49
+ updateStatus(
50
+ tenantId: string,
51
+ pluginType: string,
52
+ patch: Partial<Omit<PluginDeployment, "id" | "tenantId" | "pluginType">>
53
+ ): Promise<PluginDeployment | undefined>;
54
+ /** Deletes a deployment. Returns whether a row was removed. */
55
+ delete(tenantId: string, pluginType: string): Promise<boolean>;
56
+ }
@@ -82,10 +82,13 @@ export interface PluginConnection {
82
82
  }
83
83
 
84
84
  /**
85
- * UI declaration for a plugin tool: an HTML file rendered as an MCP App when
86
- * the tool runs.
85
+ * UI declaration for a plugin tool: an HTML resource rendered as an MCP App
86
+ * when the tool runs.
87
87
  *
88
- * @property resource - Safe relative path (within the plugin directory) to an `.html` file.
88
+ * @property resource - For GLOBAL plugins, a `ui://` key into the plugin's
89
+ * `meta.uiResources` map (inline HTML served without a sandbox read). For
90
+ * tenant sandbox plugins, a safe relative path (within the plugin directory)
91
+ * to an `.html` file on the tenant volume.
89
92
  * @property displayMode - Preferred display mode; defaults to `inline` (resolved by the compiler).
90
93
  * @property csp - Origins the UI needs (see `McpUiCsp`).
91
94
  * @property permissions - Sandbox permissions requested (see `McpUiPermissions`).
@@ -69,6 +69,12 @@ export interface ProjectBotMembership {
69
69
  roomThreadId: string;
70
70
  joinedAt: Date;
71
71
  updatedAt: Date;
72
+ /**
73
+ * True for a not-yet-materialized "virtual" public-project entry (dynamic
74
+ * roster projection). Virtual entries have no persisted row; their id
75
+ * materializes just-in-time on first interaction. Absent on real members.
76
+ */
77
+ provisional?: boolean;
72
78
  }
73
79
 
74
80
  /** Identifies a human author of a project room message. */
@@ -22,6 +22,15 @@ export interface ProjectRoomTrustedRunContext {
22
22
  * naming any specific value (ADR-118).
23
23
  */
24
24
  source?: string;
25
+ /**
26
+ * Host-provided signal that this project has no authored roster.
27
+ *
28
+ * Optional; `true` when present, `undefined`/`false` is treated as absent. Core uses it to skip
29
+ * Project roster authorization for the run's caller and owner (any tenant agent may own a task) and
30
+ * to list the tenant's agents — not the persisted roster — as available executors. The host stamps
31
+ * it only for the workspace general project. Mirrors {@link ProjectTaskTrustedRunContext.rosterExempt}.
32
+ */
33
+ rosterExempt?: boolean;
25
34
  }
26
35
 
27
36
  /** Trusted Project Task identity persisted with a privileged queue message. */
@@ -111,17 +120,21 @@ export function parseTrustedRunContext(value: unknown): TrustedRunContext {
111
120
  "tenantId", "workspaceId", "projectId", "roomId", "sourceRoomMessageId", "membershipId",
112
121
  "assistantId", "inputMessageId", "role", "title",
113
122
  ] as const;
114
- const projectRoomValues = snapshotExactRecord(projectRoomValue, requiredKeys, ["responsibility", "source"]);
123
+ const projectRoomValues = snapshotExactRecord(projectRoomValue, requiredKeys, ["responsibility", "source", "rosterExempt"]);
115
124
  const hasResponsibility = projectRoomValues !== undefined
116
125
  && Object.prototype.hasOwnProperty.call(projectRoomValues, "responsibility");
117
126
  const hasSource = projectRoomValues !== undefined
118
127
  && Object.prototype.hasOwnProperty.call(projectRoomValues, "source");
128
+ const hasRosterExempt = projectRoomValues !== undefined
129
+ && Object.prototype.hasOwnProperty.call(projectRoomValues, "rosterExempt");
119
130
  if (!projectRoomValues
120
131
  || requiredKeys.some((key) => typeof projectRoomValues[key] !== "string" || projectRoomValues[key].length === 0)
121
132
  || (hasResponsibility && projectRoomValues.responsibility !== undefined
122
133
  && (typeof projectRoomValues.responsibility !== "string" || projectRoomValues.responsibility.length === 0))
123
134
  || (hasSource && projectRoomValues.source !== undefined
124
135
  && (typeof projectRoomValues.source !== "string" || projectRoomValues.source.length === 0))
136
+ || (hasRosterExempt && projectRoomValues.rosterExempt !== undefined
137
+ && typeof projectRoomValues.rosterExempt !== "boolean")
125
138
  || (projectRoomValues.role !== "coordinator" && projectRoomValues.role !== "specialist")) {
126
139
  throw new Error("Invalid trusted agent run context");
127
140
  }
@@ -143,6 +156,9 @@ export function parseTrustedRunContext(value: unknown): TrustedRunContext {
143
156
  if (hasSource && typeof projectRoomValues.source === "string") {
144
157
  parsedProjectRoom.source = projectRoomValues.source;
145
158
  }
159
+ if (hasRosterExempt && projectRoomValues.rosterExempt === true) {
160
+ parsedProjectRoom.rosterExempt = true;
161
+ }
146
162
  return { projectRoom: parsedProjectRoom };
147
163
  }
148
164
 
@@ -257,6 +257,45 @@ describe("parseTrustedRunContext", () => {
257
257
  })).toThrow("Invalid trusted agent run context");
258
258
  });
259
259
 
260
+ it("round-trips an explicit project room roster exemption", () => {
261
+ const parsed = parseTrustedRunContext({
262
+ projectRoom: { ...validProjectRoom(), rosterExempt: true },
263
+ });
264
+
265
+ expect(parsed.projectRoom?.rosterExempt).toBe(true);
266
+ });
267
+
268
+ it("leaves project room roster exemption undefined when absent", () => {
269
+ const parsed = parseTrustedRunContext({ projectRoom: validProjectRoom() });
270
+
271
+ expect(parsed.projectRoom?.rosterExempt).toBeUndefined();
272
+ expect(Object.prototype.hasOwnProperty.call(parsed.projectRoom, "rosterExempt")).toBe(false);
273
+ });
274
+
275
+ it("treats an explicit project room roster exemption of undefined as absent", () => {
276
+ const parsed = parseTrustedRunContext({
277
+ projectRoom: { ...validProjectRoom(), rosterExempt: undefined },
278
+ });
279
+
280
+ expect(parsed.projectRoom?.rosterExempt).toBeUndefined();
281
+ expect(Object.prototype.hasOwnProperty.call(parsed.projectRoom, "rosterExempt")).toBe(false);
282
+ });
283
+
284
+ it("drops an explicit project room roster exemption of false", () => {
285
+ const parsed = parseTrustedRunContext({
286
+ projectRoom: { ...validProjectRoom(), rosterExempt: false },
287
+ });
288
+
289
+ expect(parsed.projectRoom?.rosterExempt).toBeUndefined();
290
+ expect(Object.prototype.hasOwnProperty.call(parsed.projectRoom, "rosterExempt")).toBe(false);
291
+ });
292
+
293
+ it("rejects a non-boolean project room roster exemption", () => {
294
+ expect(() => parseTrustedRunContext({
295
+ projectRoom: { ...validProjectRoom(), rosterExempt: "yes" },
296
+ })).toThrow("Invalid trusted agent run context");
297
+ });
298
+
260
299
  it.each([...Object.keys(validProjectTask()), "source"])("rejects an empty project task %s", (field) => {
261
300
  expect(() => parseTrustedRunContext({
262
301
  projectTask: { ...validProjectTask(), [field]: "" },
package/src/index.ts CHANGED
@@ -59,6 +59,7 @@ export * from "./ProjectBotMembershipStoreProtocol";
59
59
  export * from "./ProjectRoomMessageStoreProtocol";
60
60
  export * from "./ProjectRoomReadStateProtocol";
61
61
  export * from "./UserPushSubscriptionStoreProtocol";
62
+ export * from "./PluginDeploymentStoreProtocol";
62
63
  export * from "./LeaseStoreProtocol";
63
64
  export * from "./OutboxStoreProtocol";
64
65
  export * from "./ExactDataSnapshot";