@ours.network/cowork 1.2.0 → 1.3.1

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/cli.js CHANGED
@@ -4455,9 +4455,15 @@ var LowerCrockfordUlidSchema = external_exports.string().regex(
4455
4455
  );
4456
4456
  var ContainerIdSchema2 = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4457
4457
  var RuntimeCommandNameSchema = external_exports.union([external_exports.enum(RUNTIME_COMMAND_NAMES), ConsumerCommandNameSchema]);
4458
+ var RuntimeCommandNamespacePatternSchema = external_exports.string().max(128).regex(/^(?:[a-z0-9][a-z0-9-]*\.)+\*$/);
4459
+ var RuntimeCommandGrantPatternSchema = external_exports.union([
4460
+ RuntimeCommandNameSchema,
4461
+ external_exports.literal("*"),
4462
+ RuntimeCommandNamespacePatternSchema
4463
+ ]);
4458
4464
  var RuntimeCommandGrantSchema = external_exports.object({
4459
4465
  caller_cid: ContainerIdSchema2,
4460
- command: RuntimeCommandNameSchema
4466
+ command: RuntimeCommandGrantPatternSchema
4461
4467
  }).strict();
4462
4468
  function isStrictRfc3339(value) {
4463
4469
  const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
@@ -4506,7 +4512,7 @@ var RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4506
4512
  var ROOM_ROLE = "room";
4507
4513
  var RuntimeRoleCommandGrantSchema = external_exports.object({
4508
4514
  role: RoleSchema,
4509
- commands: external_exports.array(RuntimeCommandNameSchema).superRefine((commands, context) => {
4515
+ commands: external_exports.array(RuntimeCommandGrantPatternSchema).superRefine((commands, context) => {
4510
4516
  const seen = /* @__PURE__ */ new Set();
4511
4517
  for (const [index, command] of commands.entries()) {
4512
4518
  if (seen.has(command)) {
@@ -4556,6 +4562,7 @@ var SeatSchema = external_exports.object({
4556
4562
  participant_id: LowerCrockfordUlidSchema,
4557
4563
  state: SeatStateSchema,
4558
4564
  alias: NonEmptyStringSchema.optional(),
4565
+ removal_reason: external_exports.literal("contact_absent").optional(),
4559
4566
  removed_at: Rfc3339Schema.optional(),
4560
4567
  removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
4561
4568
  // Read and discard prerelease successor lineage. New rooms expose only
@@ -4569,7 +4576,7 @@ var SeatSchema = external_exports.object({
4569
4576
  context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `pending seats require ${field}` });
4570
4577
  }
4571
4578
  }
4572
- for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at"]) {
4579
+ for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at", "removal_reason"]) {
4573
4580
  if (seat[field] !== void 0) {
4574
4581
  context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `${field} is forbidden on pending seats` });
4575
4582
  }
@@ -4600,7 +4607,7 @@ var SeatSchema = external_exports.object({
4600
4607
  if (seat.accepted_at === void 0) {
4601
4608
  context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["accepted_at"], message: "active seats require accepted_at" });
4602
4609
  }
4603
- for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
4610
+ for (const field of ["removed_at", "removed_epoch", "bounced_at", "removal_reason"]) {
4604
4611
  if (seat[field] !== void 0) {
4605
4612
  context.addIssue({
4606
4613
  code: external_exports.ZodIssueCode.custom,
@@ -5643,8 +5650,8 @@ async function roomRequest(command, args) {
5643
5650
  const role = requiredFlag(parsed, "--role", "room role-command-set");
5644
5651
  const value = requiredFlag(parsed, "--commands", "room role-command-set");
5645
5652
  const commands = value === "none" ? [] : value.split(",");
5646
- if (new Set(commands).size !== commands.length || commands.some((item) => !RuntimeCommandNameSchema.safeParse(item).success)) {
5647
- usageError("--commands must be a unique comma-separated list of built-in command names or none");
5653
+ if (new Set(commands).size !== commands.length || commands.some((item) => !RuntimeCommandGrantPatternSchema.safeParse(item).success)) {
5654
+ usageError("--commands must be a unique comma-separated list of command names, * or namespace.* patterns, or none");
5648
5655
  }
5649
5656
  return { method: "room.command.role.set", params: { room_id: roomId, role, commands } };
5650
5657
  }
@@ -5655,8 +5662,8 @@ async function roomRequest(command, args) {
5655
5662
  3,
5656
5663
  `room ${command}`
5657
5664
  );
5658
- if (!RuntimeCommandNameSchema.safeParse(runtimeCommand).success) {
5659
- usageError("runtime command must be a supported built-in command name");
5665
+ if (!RuntimeCommandGrantPatternSchema.safeParse(runtimeCommand).success) {
5666
+ usageError("runtime command must be a supported command name, * or namespace.* pattern");
5660
5667
  }
5661
5668
  return {
5662
5669
  method: command === "command-grant" ? "room.command.grant" : "room.command.revoke",
package/dist/daemon.js CHANGED
@@ -11594,7 +11594,7 @@ var init_ours_runtime = __esm({
11594
11594
  }
11595
11595
  for (let settled = await step; !settled.done; settled = await step) {
11596
11596
  backoffMs = WATCH_RETRY_MIN_MS;
11597
- this.announce(identityName);
11597
+ this.announce(identityName, settled.value);
11598
11598
  step = stream.next();
11599
11599
  }
11600
11600
  if (!signal.aborted) {
@@ -11611,10 +11611,10 @@ var init_ours_runtime = __esm({
11611
11611
  backoffMs = Math.min(backoffMs * 2, WATCH_RETRY_MAX_MS);
11612
11612
  }
11613
11613
  }
11614
- announce(identityName) {
11614
+ announce(identityName, event) {
11615
11615
  for (const listener of this.listeners) {
11616
11616
  try {
11617
- listener(identityName);
11617
+ listener(identityName, event);
11618
11618
  } catch (error) {
11619
11619
  this.log(`cowork SDK notification listener failed for ${identityName}:`, error);
11620
11620
  }
@@ -11625,6 +11625,9 @@ var init_ours_runtime = __esm({
11625
11625
  });
11626
11626
 
11627
11627
  // src/command-names.ts
11628
+ function commandGrantMatches(pattern, command) {
11629
+ return pattern === command || pattern === "*" || pattern.endsWith(".*") && command.startsWith(pattern.slice(0, -1)) && command.length > pattern.length - 1;
11630
+ }
11628
11631
  var SHARED_ROOM_COMMANDS, RUNTIME_COMMAND_NAMES;
11629
11632
  var init_command_names = __esm({
11630
11633
  "src/command-names.ts"() {
@@ -12050,7 +12053,7 @@ function refineMessageThread(message, context) {
12050
12053
  });
12051
12054
  }
12052
12055
  }
12053
- var MAX_TEXT_BYTES, MAX_FILE_BYTES, MAX_HISTORY_PAGE_BYTES, MAX_MANAGEMENT_RESPONSE_BYTES, MAX_EXTERNAL_INVITE_BYTES, MAX_FILE_NAME_BYTES, MAX_MIME_BYTES, MAX_ROLE_BYTES, MAX_ROOM_NAME_CHARACTERS, MAX_ROOM_IDENTITY_NAME_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, ContainerIdSchema2, RuntimeCommandNameSchema, RuntimeCommandGrantSchema, Rfc3339Schema, RoomNameSchema, ROOM_IDENTITY_PREFIX, MAX_ROOM_IDENTITY_TITLE_CHARACTERS, SDK_IDENTITY_NAME_FORBIDDEN, SDK_IDENTITY_NAME_RESERVED, CoworkIdentityNameError, RoleSchema, ROOM_ROLE, RuntimeRoleCommandGrantSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, CurrentRoomSchema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, PostAsRoleInputSchema, RestRoleInputSchema, AcceptExternalInviteInputSchema, ListMembersCommandInputSchema, CommandIdempotencyKeySchema, RemoveMemberCommandInputSchema, RuntimeCommandGrantInputSchema, RuntimeRoleCommandGrantInputSchema, RuntimeCommandAuditSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, ReplyReferenceSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, IntakeRejectionShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, IntakeRejectionRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
12056
+ var MAX_TEXT_BYTES, MAX_FILE_BYTES, MAX_HISTORY_PAGE_BYTES, MAX_MANAGEMENT_RESPONSE_BYTES, MAX_EXTERNAL_INVITE_BYTES, MAX_FILE_NAME_BYTES, MAX_MIME_BYTES, MAX_ROLE_BYTES, MAX_ROOM_NAME_CHARACTERS, MAX_ROOM_IDENTITY_NAME_CHARACTERS, NonEmptyStringSchema, PositiveSafeIntegerSchema, LowerCrockfordUlidSchema, ContainerIdSchema2, RuntimeCommandNameSchema, RuntimeCommandNamespacePatternSchema, RuntimeCommandGrantPatternSchema, RuntimeCommandGrantSchema, Rfc3339Schema, RoomNameSchema, ROOM_IDENTITY_PREFIX, MAX_ROOM_IDENTITY_TITLE_CHARACTERS, SDK_IDENTITY_NAME_FORBIDDEN, SDK_IDENTITY_NAME_RESERVED, CoworkIdentityNameError, RoleSchema, ROOM_ROLE, RuntimeRoleCommandGrantSchema, MissionTextSchema, MessageTextSchema, FileNameSchema, FileMimeSchema, RoomStateSchema, SeatStateSchema, InviteModeSchema, DEFAULT_ROLE, InviteStateSchema, RelayStatusSchema, SeatV1Schema, SeatSchema, RoomInviteSchema, MissionV1Schema, MissionSchema, RoleBriefingSchema, RoomCommonShape, RoomV1Schema, CurrentRoomSchema, RoomSchema, CreateRoomInputSchema, UpdateRoomInputSchema, RoleBriefingSetInputSchema, RoleBriefingDeleteInputSchema, PostMessageInputSchema, PostAsRoleInputSchema, RestRoleInputSchema, AcceptExternalInviteInputSchema, ListMembersCommandInputSchema, CommandIdempotencyKeySchema, RemoveMemberCommandInputSchema, RuntimeCommandGrantInputSchema, RuntimeRoleCommandGrantInputSchema, RuntimeCommandAuditSchema, AuthorSnapshotSchema, RecordCommonShape, AppendCommonShape, MembershipNoticeSchema, AuthorAliasSchema, ReplyReferenceSchema, MessageShape, RelayIntentShape, RelayResultStatusSchema, RelayResultShape, FileShape, IntakeRejectionShape, MembershipIntentShape, MembershipResultShape, CloseNoticeIntentShape, CloseNoticeResultShape, MessageRecordSchema, FileRecordSchema, RelayIntentRecordSchema, RelayResultRecordSchema, IntakeRejectionRecordSchema, MembershipIntentRecordSchema, MembershipResultRecordSchema, CloseNoticeIntentRecordSchema, CloseNoticeResultRecordSchema, RawCommunicationRecordSchema, CommunicationRecordSchema, AppendRecordSchema;
12054
12057
  var init_contracts = __esm({
12055
12058
  "src/contracts.ts"() {
12056
12059
  "use strict";
@@ -12076,9 +12079,15 @@ var init_contracts = __esm({
12076
12079
  );
12077
12080
  ContainerIdSchema2 = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
12078
12081
  RuntimeCommandNameSchema = external_exports.union([external_exports.enum(RUNTIME_COMMAND_NAMES), ConsumerCommandNameSchema]);
12082
+ RuntimeCommandNamespacePatternSchema = external_exports.string().max(128).regex(/^(?:[a-z0-9][a-z0-9-]*\.)+\*$/);
12083
+ RuntimeCommandGrantPatternSchema = external_exports.union([
12084
+ RuntimeCommandNameSchema,
12085
+ external_exports.literal("*"),
12086
+ RuntimeCommandNamespacePatternSchema
12087
+ ]);
12079
12088
  RuntimeCommandGrantSchema = external_exports.object({
12080
12089
  caller_cid: ContainerIdSchema2,
12081
- command: RuntimeCommandNameSchema
12090
+ command: RuntimeCommandGrantPatternSchema
12082
12091
  }).strict();
12083
12092
  Rfc3339Schema = external_exports.string().refine(isStrictRfc3339, "must be a valid RFC3339 timestamp");
12084
12093
  RoomNameSchema = external_exports.string().refine(
@@ -12108,7 +12117,7 @@ var init_contracts = __esm({
12108
12117
  ROOM_ROLE = "room";
12109
12118
  RuntimeRoleCommandGrantSchema = external_exports.object({
12110
12119
  role: RoleSchema,
12111
- commands: external_exports.array(RuntimeCommandNameSchema).superRefine((commands, context) => {
12120
+ commands: external_exports.array(RuntimeCommandGrantPatternSchema).superRefine((commands, context) => {
12112
12121
  const seen = /* @__PURE__ */ new Set();
12113
12122
  for (const [index, command] of commands.entries()) {
12114
12123
  if (seen.has(command)) {
@@ -12159,6 +12168,7 @@ var init_contracts = __esm({
12159
12168
  participant_id: LowerCrockfordUlidSchema,
12160
12169
  state: SeatStateSchema,
12161
12170
  alias: NonEmptyStringSchema.optional(),
12171
+ removal_reason: external_exports.literal("contact_absent").optional(),
12162
12172
  removed_at: Rfc3339Schema.optional(),
12163
12173
  removed_epoch: external_exports.number().int().nonnegative().safe().optional(),
12164
12174
  // Read and discard prerelease successor lineage. New rooms expose only
@@ -12172,7 +12182,7 @@ var init_contracts = __esm({
12172
12182
  context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `pending seats require ${field}` });
12173
12183
  }
12174
12184
  }
12175
- for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at"]) {
12185
+ for (const field of ["accepted_at", "removed_at", "removed_epoch", "bounced_at", "removal_reason"]) {
12176
12186
  if (seat[field] !== void 0) {
12177
12187
  context.addIssue({ code: external_exports.ZodIssueCode.custom, path: [field], message: `${field} is forbidden on pending seats` });
12178
12188
  }
@@ -12203,7 +12213,7 @@ var init_contracts = __esm({
12203
12213
  if (seat.accepted_at === void 0) {
12204
12214
  context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["accepted_at"], message: "active seats require accepted_at" });
12205
12215
  }
12206
- for (const field of ["removed_at", "removed_epoch", "bounced_at"]) {
12216
+ for (const field of ["removed_at", "removed_epoch", "bounced_at", "removal_reason"]) {
12207
12217
  if (seat[field] !== void 0) {
12208
12218
  context.addIssue({
12209
12219
  code: external_exports.ZodIssueCode.custom,
@@ -12973,7 +12983,7 @@ function apiDocsAsset(pathname) {
12973
12983
  return void 0;
12974
12984
  }
12975
12985
  }
12976
- var RPC_PATH, OPENAPI_DOCUMENT_PATH, API_DOCS_PATH, API_DOCS_SCRIPT_PATH, API_DOCS_STYLESHEET_PATH, API_VERSION, EXAMPLE_ROOM_ID, roomIdProperty, roleProperty, restRoleProperty, missionTextProperty, roomNameProperty, inviteModeProperty, notifyProperty, PRIVATE_ROOM_RPC_METHODS, ROOM_RPC_METHODS, RPC_ERROR_CODES, DOCS_PAGE, DOCS_STYLESHEET, DOCS_SCRIPT;
12986
+ var RPC_PATH, OPENAPI_DOCUMENT_PATH, API_DOCS_PATH, API_DOCS_SCRIPT_PATH, API_DOCS_STYLESHEET_PATH, API_VERSION, commandGrantPatternProperty, EXAMPLE_ROOM_ID, roomIdProperty, roleProperty, restRoleProperty, missionTextProperty, roomNameProperty, inviteModeProperty, notifyProperty, PRIVATE_ROOM_RPC_METHODS, ROOM_RPC_METHODS, RPC_ERROR_CODES, DOCS_PAGE, DOCS_STYLESHEET, DOCS_SCRIPT;
12977
12987
  var init_openapi = __esm({
12978
12988
  "src/openapi.ts"() {
12979
12989
  "use strict";
@@ -12984,6 +12994,15 @@ var init_openapi = __esm({
12984
12994
  API_DOCS_SCRIPT_PATH = "/docs/ui.js";
12985
12995
  API_DOCS_STYLESHEET_PATH = "/docs/ui.css";
12986
12996
  API_VERSION = "1";
12997
+ commandGrantPatternProperty = {
12998
+ anyOf: [
12999
+ { type: "string", enum: [...RUNTIME_COMMAND_NAMES] },
13000
+ { type: "string", maxLength: 128, pattern: "^consumer\\.[a-z0-9][a-z0-9.-]*[a-z0-9]$" },
13001
+ { type: "string", enum: ["*"] },
13002
+ { type: "string", maxLength: 128, pattern: "^(?:[a-z0-9][a-z0-9-]*\\.)+\\*$" }
13003
+ ],
13004
+ description: "Exact command, * for all runtime commands, or terminal namespace.*. Patterns also cover future matching commands."
13005
+ };
12987
13006
  EXAMPLE_ROOM_ID = "01jd7q4h9m2v8xk3znbc5regty";
12988
13007
  roomIdProperty = {
12989
13008
  type: "string",
@@ -13048,7 +13067,7 @@ var init_openapi = __esm({
13048
13067
  {
13049
13068
  method: "room.command.definition.put",
13050
13069
  summary: "Register or replace a consumer command",
13051
- description: "Registers a consumer.* command for the ours catalog using an exact host-configured handler reference. Replacement clears grants for that name. Local definitions cannot be replaced here. A published:false result means desired state is committed; use reload to retry publication.",
13070
+ description: "Registers a consumer.* command for the ours catalog using an exact host-configured handler reference. Replacement clears exact grants for that name; wildcard grants remain effective. Local definitions cannot be replaced here. A published:false result means desired state is committed; use reload to retry publication.",
13052
13071
  params: params({ room_id: roomIdProperty, expected_revision: { type: "integer", minimum: 0 }, definition: {
13053
13072
  type: "object",
13054
13073
  additionalProperties: false,
@@ -13061,7 +13080,7 @@ var init_openapi = __esm({
13061
13080
  {
13062
13081
  method: "room.command.definition.delete",
13063
13082
  summary: "Delete a REST consumer command",
13064
- description: "Deletes the desired definition and its CID/role grants. Requires the current registry revision. Local commands must be removed from their file and reloaded.",
13083
+ description: "Deletes the desired definition and its exact CID/role grants; wildcard grants remain stored. Requires the current registry revision. Local commands must be removed from their file and reloaded.",
13065
13084
  params: params({ room_id: roomIdProperty, expected_revision: { type: "integer", minimum: 0 }, name: { type: "string" } }, ["room_id", "expected_revision", "name"]),
13066
13085
  result: "Updated revision, published and definitions.",
13067
13086
  example: { room_id: EXAMPLE_ROOM_ID, expected_revision: 1, name: "consumer.orders" }
@@ -13231,7 +13250,7 @@ var init_openapi = __esm({
13231
13250
  {
13232
13251
  method: "room.command.grants",
13233
13252
  summary: "List runtime-command grants",
13234
- description: "Lists the default-deny grants that bind one active participant CID to one room runtime command.",
13253
+ description: "Lists the default-deny grants that bind one active participant CID to one room runtime command name or stored wildcard pattern.",
13235
13254
  params: params({ room_id: roomIdProperty }, ["room_id"]),
13236
13255
  result: "An array of `{caller_cid, command}` grants.",
13237
13256
  example: { room_id: EXAMPLE_ROOM_ID }
@@ -13247,13 +13266,11 @@ var init_openapi = __esm({
13247
13266
  {
13248
13267
  method: "room.command.role.set",
13249
13268
  summary: "Set a role runtime-command policy",
13250
- description: "Atomically replaces the command policy for one exact role. An empty command list removes the policy without removing independent per-CID grants.",
13269
+ description: "Atomically replaces the command policy for one exact role. An empty command list removes the policy without removing independent per-CID grants. Accepts exact names, * and namespace.* patterns.",
13251
13270
  params: params({
13252
13271
  room_id: roomIdProperty,
13253
13272
  role: { type: "string", minLength: 1, description: "Exact admitted seat role." },
13254
- commands: { type: "array", uniqueItems: true, items: {
13255
- anyOf: [{ type: "string", enum: [...RUNTIME_COMMAND_NAMES] }, { type: "string", maxLength: 128, pattern: "^consumer\\.[a-z0-9][a-z0-9.-]*[a-z0-9]$" }]
13256
- } }
13273
+ commands: { type: "array", uniqueItems: true, items: commandGrantPatternProperty }
13257
13274
  }, ["room_id", "role", "commands"]),
13258
13275
  result: "The complete sorted role-policy list after the idempotent update.",
13259
13276
  example: { room_id: EXAMPLE_ROOM_ID, role: "Owner", commands: ["list-members", "remove-member"] }
@@ -13261,11 +13278,11 @@ var init_openapi = __esm({
13261
13278
  {
13262
13279
  method: "room.command.grant",
13263
13280
  summary: "Authorize a runtime command caller",
13264
- description: "Idempotently grants one command to one exact active participant CID. Display names and roles are never authority.",
13281
+ description: "Idempotently grants one command name or dynamic pattern to one exact active participant CID. Display names and caller-supplied roles are never authority. * and room.* include permission administration and room lifecycle commands; * also covers future or replaced consumer commands.",
13265
13282
  params: params({
13266
13283
  room_id: roomIdProperty,
13267
13284
  caller_cid: { type: "string", pattern: "^[0-9A-Fa-f]{64}$", description: "Authenticated caller CID." },
13268
- command: { anyOf: [{ type: "string", enum: [...RUNTIME_COMMAND_NAMES] }, { type: "string", maxLength: 128, pattern: "^consumer\\.[a-z0-9][a-z0-9.-]*[a-z0-9]$" }] }
13285
+ command: commandGrantPatternProperty
13269
13286
  }, ["room_id", "caller_cid", "command"]),
13270
13287
  result: "The complete sorted grant list after the idempotent update.",
13271
13288
  example: { room_id: EXAMPLE_ROOM_ID, caller_cid: "A".repeat(64), command: "list-members" }
@@ -13273,11 +13290,11 @@ var init_openapi = __esm({
13273
13290
  {
13274
13291
  method: "room.command.revoke",
13275
13292
  summary: "Revoke a runtime command caller",
13276
- description: "Idempotently removes one exact CID/command grant. Revocation is persisted before subsequent command dispatch can acquire the room mutex.",
13293
+ description: "Idempotently removes only the exact stored CID/name or CID/pattern entry; overlapping grants remain effective. Revocation is persisted before subsequent command dispatch can acquire the room mutex.",
13277
13294
  params: params({
13278
13295
  room_id: roomIdProperty,
13279
13296
  caller_cid: { type: "string", pattern: "^[0-9A-Fa-f]{64}$", description: "Authenticated caller CID." },
13280
- command: { anyOf: [{ type: "string", enum: [...RUNTIME_COMMAND_NAMES] }, { type: "string", maxLength: 128, pattern: "^consumer\\.[a-z0-9][a-z0-9.-]*[a-z0-9]$" }] }
13297
+ command: commandGrantPatternProperty
13281
13298
  }, ["room_id", "caller_cid", "command"]),
13282
13299
  result: "The complete sorted grant list after the idempotent update.",
13283
13300
  example: { room_id: EXAMPLE_ROOM_ID, caller_cid: "A".repeat(64), command: "remove-member" }
@@ -13774,12 +13791,13 @@ var init_packets = __esm({
13774
13791
  });
13775
13792
  this.rebindSleep = options.rebindSleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
13776
13793
  this.rebindRandom = options.rebindRandom ?? Math.random;
13777
- this.unsubscribe = host.onIdentityNotify((name) => {
13794
+ this.unsubscribe = host.onIdentityNotify((name, event) => {
13778
13795
  const found = [...this.packets.entries()].find(([, packet2]) => packet2.name === name);
13779
13796
  if (!found) return;
13780
13797
  const [roomId, packet] = found;
13798
+ if (event?.event === "contact_removed" && event.identity_cid !== packet.cid) return;
13781
13799
  void packet.refresh().then(
13782
- () => this.onNotify(roomId, "message_received"),
13800
+ () => this.onNotify(roomId, event?.event === "contact_removed" ? "contact_removed" : "message_received"),
13783
13801
  (error) => this.log(`[${name}] failed to refresh SDK state after notification:`, error)
13784
13802
  );
13785
13803
  });
@@ -15027,6 +15045,17 @@ var init_intake = __esm({
15027
15045
  const room = await this.store.load(roomId);
15028
15046
  const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
15029
15047
  const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
15048
+ let firstDeliveryError;
15049
+ let deliveryFailed = false;
15050
+ const attempt = async (send) => {
15051
+ try {
15052
+ return await send();
15053
+ } catch (error) {
15054
+ if (!deliveryFailed) firstDeliveryError = error;
15055
+ deliveryFailed = true;
15056
+ return void 0;
15057
+ }
15058
+ };
15030
15059
  let after = 0;
15031
15060
  for (; ; ) {
15032
15061
  const pending = await queryStore(this.store, roomId, {
@@ -15035,7 +15064,10 @@ var init_intake = __esm({
15035
15064
  after,
15036
15065
  limit: JOURNAL_WORK_BATCH_SIZE
15037
15066
  });
15038
- if (pending.length === 0) return;
15067
+ if (pending.length === 0) {
15068
+ if (deliveryFailed) throw firstDeliveryError;
15069
+ return;
15070
+ }
15039
15071
  for (const intent of pending) {
15040
15072
  after = intent.seq;
15041
15073
  const [message] = intent.message_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "message", messageId: intent.message_id, limit: 1 });
@@ -15082,7 +15114,7 @@ var init_intake = __esm({
15082
15114
  const replyTo = decision.replyTo;
15083
15115
  if (file !== void 0) {
15084
15116
  const uploader = file.author_alias?.alias ?? file.author.display_name;
15085
- const notice = await sendRoomBody(packet, intent.recipient_identity, {
15117
+ const notice = await attempt(() => sendRoomBody(packet, intent.recipient_identity, {
15086
15118
  version: 1,
15087
15119
  kind: "room_msg",
15088
15120
  room_id: roomId,
@@ -15095,7 +15127,8 @@ var init_intake = __esm({
15095
15127
  },
15096
15128
  text: `${uploader} sent a file`,
15097
15129
  at: file.at
15098
- }, replyTo);
15130
+ }, replyTo));
15131
+ if (notice === void 0) continue;
15099
15132
  if (notice.status === "send_failed") {
15100
15133
  const failed = await this.store.append(roomId, {
15101
15134
  version: 1,
@@ -15110,13 +15143,14 @@ var init_intake = __esm({
15110
15143
  if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
15111
15144
  continue;
15112
15145
  }
15113
- const outcome2 = await packet.sendFile(
15146
+ const outcome2 = await attempt(() => packet.sendFile(
15114
15147
  intent.recipient_identity,
15115
15148
  file.filename,
15116
15149
  file.mime,
15117
15150
  Buffer.from(file.data_base64, "base64"),
15118
15151
  replyTo
15119
- );
15152
+ ));
15153
+ if (outcome2 === void 0) continue;
15120
15154
  const appended2 = await this.store.append(roomId, {
15121
15155
  version: 1,
15122
15156
  kind: "relay_result",
@@ -15151,7 +15185,8 @@ var init_intake = __esm({
15151
15185
  ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
15152
15186
  ...message.membership === void 0 ? {} : { membership: message.membership }
15153
15187
  };
15154
- const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned, replyTo);
15188
+ const outcome = await attempt(() => sendRoomBody(packet, intent.recipient_identity, unsigned, replyTo));
15189
+ if (outcome === void 0) continue;
15155
15190
  const appended = await this.store.append(roomId, {
15156
15191
  version: 1,
15157
15192
  kind: "relay_result",
@@ -15375,12 +15410,12 @@ var init_command_routes = __esm({
15375
15410
  RuntimeCommandGrantParams = external_exports.object({
15376
15411
  room_id: external_exports.string(),
15377
15412
  caller_cid: ContainerIdSchema2,
15378
- command: RuntimeCommandNameSchema
15413
+ command: RuntimeCommandGrantPatternSchema
15379
15414
  }).strict();
15380
15415
  RuntimeRoleCommandGrantParams = external_exports.object({
15381
15416
  room_id: external_exports.string(),
15382
15417
  role: external_exports.string(),
15383
- commands: external_exports.array(RuntimeCommandNameSchema)
15418
+ commands: external_exports.array(RuntimeCommandGrantPatternSchema)
15384
15419
  }).strict();
15385
15420
  ParticipantRemoveParams = external_exports.object({
15386
15421
  room_id: external_exports.string(),
@@ -16265,9 +16300,9 @@ var init_service = __esm({
16265
16300
  return seat;
16266
16301
  }
16267
16302
  hasRuntimeCommandGrant(room, callerCid, command) {
16268
- if (room.command_grants.some((grant) => grant.caller_cid === callerCid && grant.command === command)) return true;
16303
+ if (room.command_grants.some((grant) => grant.caller_cid === callerCid && commandGrantMatches(grant.command, command))) return true;
16269
16304
  const caller = room.seats.find((seat) => seat.state === "active" && seat.identity === callerCid);
16270
- return caller !== void 0 && room.role_command_grants.some((grant) => grant.role === caller.role && grant.commands.includes(command));
16305
+ return caller !== void 0 && room.role_command_grants.some((grant) => grant.role === caller.role && grant.commands.some((pattern) => commandGrantMatches(pattern, command)));
16271
16306
  }
16272
16307
  async beginRemovalUnlocked(room, seat, notify) {
16273
16308
  if (seat.state === "pending" || isCancelledExternalSeat(seat)) {
@@ -16575,7 +16610,7 @@ var init_service = __esm({
16575
16610
  }
16576
16611
  /** List the operator-managed runtime-command grants for one room. */
16577
16612
  assertRegisteredConsumerName(room, name) {
16578
- if (name.startsWith("consumer.") && !(room.consumer_commands ?? []).some((definition) => definition.name === name)) {
16613
+ if (name.startsWith("consumer.") && !name.endsWith(".*") && !(room.consumer_commands ?? []).some((definition) => definition.name === name)) {
16579
16614
  throw new RoomServiceError("consumer command is not registered");
16580
16615
  }
16581
16616
  }
@@ -16719,7 +16754,7 @@ var init_service = __esm({
16719
16754
  return saved.role_command_grants.map((grant) => ({ role: grant.role, commands: [...grant.commands] }));
16720
16755
  });
16721
16756
  }
16722
- /** Grant one exact command to one active authenticated room identity. Idempotent. */
16757
+ /** Grant one command selector to one active authenticated room identity. Idempotent. */
16723
16758
  async grantRuntimeCommand(roomId, input) {
16724
16759
  const id = LowerCrockfordUlidSchema.parse(roomId);
16725
16760
  const request = RuntimeCommandGrantInputSchema.parse(input);
@@ -16738,7 +16773,7 @@ var init_service = __esm({
16738
16773
  return saved.command_grants.map((grant) => ({ ...grant }));
16739
16774
  });
16740
16775
  }
16741
- /** Revoke one exact command grant. Absence is already the requested state. */
16776
+ /** Remove only the exact stored selector; overlapping grants remain effective. */
16742
16777
  async revokeRuntimeCommand(roomId, input) {
16743
16778
  const id = LowerCrockfordUlidSchema.parse(roomId);
16744
16779
  const request = RuntimeCommandGrantInputSchema.parse(input);
@@ -16976,6 +17011,7 @@ var init_service = __esm({
16976
17011
  }
16977
17012
  async reconcileUnlocked(room, packet) {
16978
17013
  if (room.state === "closed" || room.state === "closing") return room;
17014
+ await packet.refreshContacts();
16979
17015
  const contactsByCid = /* @__PURE__ */ new Map();
16980
17016
  for (const contact of packet.listContacts()) {
16981
17017
  if (contactsByCid.has(contact.container_id)) continue;
@@ -16985,6 +17021,27 @@ var init_service = __esm({
16985
17021
  ...authenticatedInvite === void 0 ? {} : { inviteId: authenticatedInvite }
16986
17022
  });
16987
17023
  }
17024
+ let epoch = room.membership_epoch;
17025
+ const departed = /* @__PURE__ */ new Set();
17026
+ const reconciledSeats = room.seats.map((seat) => {
17027
+ if (seat.state !== "active" || contactsByCid.has(seat.identity)) return seat;
17028
+ departed.add(seat.identity);
17029
+ return {
17030
+ ...seat,
17031
+ state: "removed",
17032
+ removed_at: this.now(),
17033
+ removed_epoch: ++epoch,
17034
+ removal_reason: "contact_absent"
17035
+ };
17036
+ });
17037
+ if (departed.size > 0) {
17038
+ room = await this.store.save(RoomSchema.parse({
17039
+ ...room,
17040
+ seats: reconciledSeats,
17041
+ membership_epoch: epoch,
17042
+ command_grants: room.command_grants.filter((grant) => !departed.has(grant.caller_cid))
17043
+ }));
17044
+ }
16988
17045
  const inviteById = new Map(room.invites.map((invite) => [invite.invite_id, invite]));
16989
17046
  const existingCids = new Set(room.seats.filter((seat) => seat.state === "active" || seat.state === "pending").map((seat) => seat.identity));
16990
17047
  const lastRemovedAt = /* @__PURE__ */ new Map();
@@ -18978,7 +19035,7 @@ import * as nodeFs5 from "node:fs";
18978
19035
  import { join as join7 } from "node:path";
18979
19036
  import { fileURLToPath } from "node:url";
18980
19037
  function isIntakeNotification(event) {
18981
- return event === "message_received" || event === "file_received" || event === "contact_accepted" || event === "contact_added";
19038
+ return event === "message_received" || event === "file_received" || event === "contact_accepted" || event === "contact_added" || event === "contact_removed";
18982
19039
  }
18983
19040
  function createDaemonControlRoutes(control) {
18984
19041
  if (!/^[0-9a-f]{32}$/.test(control.session)) throw new TypeError("invalid daemon control session");
@@ -14,9 +14,51 @@ Update the display name with `ours-cowork room settings <room-id> --name "New na
14
14
 
15
15
  Runtime commands are default-deny. Per-CID grants use `command-grant` and `command-revoke`. An operator may instead register a durable role policy with `role-command-set <room-id> --role <label> --commands <comma-list>` and inspect it with `role-command-grants`; use `--commands none` to remove it. A role policy authorizes only an authenticated, active seat whose durable admission role exactly matches the policy. Display names, message labels, pending seats, removed seats, and caller-supplied role text never confer authority. Removing a role policy does not remove an independently configured per-CID grant.
16
16
 
17
+ ### Wildcard command grants
18
+
19
+ CID grants and role policies accept exact command names, `*`, and a terminal
20
+ `namespace.*` pattern. Quote patterns in the shell:
21
+
22
+ ```sh
23
+ ours-cowork room command-grant <room-id> <caller-cid> '*'
24
+ ours-cowork room command-grant <room-id> <caller-cid> 'room.*'
25
+ ours-cowork room role-command-set <room-id> --role Reviewer --commands 'room.briefing.*,consumer.orders.*'
26
+ ours-cowork room command-revoke <room-id> <caller-cid> 'room.*'
27
+ ```
28
+
29
+ Use the same strings in the `command` field of `room.command.grant` / `revoke`
30
+ and the `commands` array of `room.command.role.set` over RPC or ours transport.
31
+ Invocation still requires a concrete command name; patterns are permissions only.
32
+
33
+ `*` covers all room runtime commands, including `start_thread`, `list-members`,
34
+ `remove-member`, shared `room.*` commands and registered `consumer.*` commands.
35
+ `room.*` covers only names beginning with `room.`; it excludes those dedicated
36
+ commands and consumer commands. `room.command.*` includes deeper descendants.
37
+ Namespace segments use lowercase ASCII letters, digits and hyphens, start with a
38
+ letter or digit, and are separated by dots; a namespace pattern is at most 128
39
+ characters. Empty segments, internal stars, `room*`, `*.show`, regex, whitespace
40
+ and case folding are unsupported. A valid namespace need not exist yet.
41
+
42
+ Patterns are stored and listed verbatim, and match future commands dynamically.
43
+ Consumer replacement/deletion still clears exact grants for the changed name,
44
+ but retains wildcard grants: once a matching consumer is registered and published
45
+ again, the wildcard authorizes it. Registration and publication checks still apply.
46
+
47
+ `*` and `room.*` include permission administration and room close/delete commands,
48
+ so their holders can delegate privileges. Granting them requires the same management
49
+ access or existing permission to invoke the relevant grant command. Patterns never
50
+ bypass authenticated CID, active membership, room scope, lifecycle checks, or
51
+ participant result visibility; they do not authorize host/global management APIs.
52
+
53
+ Granting an identical CID/pattern is idempotent. Revocation removes only the exact
54
+ stored entry, not all matching names: revoking `room.show` does not override a retained
55
+ `room.*`, and revoking `room.*` leaves independent exact or role grants in effect.
56
+ Patterns survive restart, and per-CID grants are removed with participant removal
57
+ just like exact grants. Empty role command lists remove the role policy.
58
+
17
59
  ## Scoped reply threads
18
60
 
19
- The dedicated `start_thread` runtime command creates a scoped reply thread after an operator grants that exact command name. Discover and invoke it through the SDK's generic command APIs, then use the SDK's native reply field:
61
+ The dedicated `start_thread` runtime command creates a scoped reply thread after an operator grants `start_thread` or `*`. Discover and invoke it through the SDK's generic command APIs, then use the SDK's native reply field:
20
62
 
21
63
  ```ts
22
64
  const definitions = await client.listContactCommands({ contact: roomCid });
@@ -66,10 +108,24 @@ Every web action has an equivalent CLI fallback in the room commands above and i
66
108
 
67
109
  Room-scoped operations also appear in the room identity's ours catalog with their RPC names: `room.settings`, `room.briefing.role.set`, `room.briefing.role.delete`, `room.invite`, `room.participant.remove`, `room.revoke`, `room.recover`, `room.recover.confirm`, `room.show`, `room.participants`, `room.command.grants`, `room.command.role.grants`, `room.command.role.set`, `room.command.grant`, `room.command.revoke`, `room.history`, `room.message`, `room.say`, `room.role.rest.add`, `room.role.rest.remove`, `room.accept`, `room.rebind`, `room.close`, and `room.delete`.
68
110
 
69
- Use the RPC arguments without `room_id`; the receiving room fixes the target. For example, an operator grants `ours-cowork room command-grant <room-id> <caller-cid> room.settings`, then that active member calls `room.settings` with `{"status":"review"}` using ours command transport. A grant for one name grants none of the other names. The SDK returns a correlated result containing `{ok:true,result:<service value>}` or `{ok:false,error:<code>}`. History returns one page; follow `seq` with `after` to fetch more.
111
+ Use the RPC arguments without `room_id`; the receiving room fixes the target. For example, an operator grants `ours-cowork room command-grant <room-id> <caller-cid> room.settings`, then that active member calls `room.settings` with `{"status":"review"}` using ours command transport. An exact-name grant grants none of the other names. The SDK returns a correlated result containing `{ok:true,result:<service value>}` or `{ok:false,error:<code>}`. History returns one page; follow `seq` with `after` to fetch more.
70
112
 
71
113
  `start_thread`, `list-members`, and `remove-member` are dedicated runtime commands rather than shared management routes. `list-members` retains its contact-safe roster. `remove-member` retains its epoch, confirm and no-self-removal gates. The separate `room.participant.remove` command instead grants the full operator removal behavior. `room.show` returns only public room settings and mission content; `room.participants` returns participant IDs, roles, and states. Runtime `room.history` returns only messages visible to the authenticated active seat, with viewer-local cursors, and rejects operator view. Runtime `room.message` and `room.say` return only an accepted message-ID receipt. Host management routes retain their full operator results. Policy-administration commands can delegate more privileges; `room.message` and `room.say` authorize room/role authorship. Assign these permissions deliberately. Command results may include invite material; do not relay them into chat.
72
114
 
73
115
  Host lifecycle and global room creation/listing are excluded. `room.accept` accepts invitation input through its separate grant and remains unavailable through REST. Ours close/delete return a durable accepted receipt before closing the reply channel; verify completion through management. Close retains archive/files; delete requires `confirm:true`, closes first, and erases local room data. Pending lifecycle requests resume after restart; failed requests remain visible in room metadata for explicit management retry. Missing replies do not prove a mutation failed.
74
116
 
75
117
  The SDK owns command reply delivery and its size limits. Large history pages or a single large file record may exceed that transport's capacity; use CLI/REST for those results. No additional result-omission contract is introduced.
118
+
119
+ ## Contact deletion and room membership
120
+
121
+ Cowork subscribes to SDK notifications for each hosted room identity. A typed `contact_removed` with a different local identity CID is ignored, even when the identity name matches. The room packet refreshes state and forwards the wake. Under the room mutex, reconciliation refreshes contacts again and removes only active seats whose exact peer CID is absent. The same path runs on startup, watch reconnection and the existing periodic resync, covering missed notifications and a crash between core deletion and room persistence.
122
+
123
+ A departed seat stays in durable room metadata with its participant ID, CID, role, accepted_at, invite provenance, removed_at, removed_epoch and `removal_reason: 'contact_absent'`. Each transition increments membership_epoch once and revokes that CID's explicit command grants. Accepted invite history remains accepted: a departure is not an invitation/admission failure. Pending asynchronous admissions without contacts stay pending. Removal reason deliberately does not claim peer intent: contact absence can also follow local removal or crash recovery. This persisted lifecycle record remains available even in quiet rooms and when nobody remains to receive a membership message.
124
+
125
+ Duplicate wakes and restarts do not change an already-removed seat's epoch or timestamp. An old event with a currently present/re-added contact cannot remove it. No attempt is made to infer an unobserved remove-and-readd cycle from a surviving contact snapshot. Each room identity reconciles its own contact book; deleting a contact in one room does not authorize removal in another where that contact still exists.
126
+
127
+ Future broadcasts use active membership; pending relays already addressed to departed participants use the existing `skipped_removed` outcome. If a contact disappears during delivery, independent recipients are still attempted (text, file metadata and file bytes, including later journal batches). A thrown send remains result-less and is retried under existing recovery semantics; the first error is reported after other recipients have been attempted. Storage errors stop immediately. No durable append receipt or relay outcome schema was changed. This boundary was coordinated with backlog task `0mtzfmkqra63687cd`, which remains owner of durable receipts and per-recipient outcome design.
128
+
129
+ ### Rollout
130
+
131
+ No core protocol or room-version migration is required. Existing version-2 rooms load with optional `removal_reason` absent; reconciliation fills it only on newly observed contact departures. Older Cowork versions strip this optional field on parse/save and do not perform automatic reconciliation; retain the upgraded version to preserve reason metadata. Admission and removal fields use the existing room lifecycle schema. An older SDK daemon supplies a legacy/sync wake; authoritative periodic reconciliation still works. The enriched typed event requires the accompanying SDK daemon change. No dependency bump to an unpublished package is made by this patch; release the SDK enhancement before or alongside Cowork.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/cowork",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Durable ours mission rooms on the shared ours daemon.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",