@ours.network/cowork 1.1.4 → 1.3.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/README.md CHANGED
@@ -23,6 +23,8 @@ The package keeps room metadata and a per-room indexed `archive.sqlite3` in its
23
23
 
24
24
  Ordinary ours-mcp identities can join only as remote participants over the ours protocol.
25
25
 
26
+ Granted room members can create message-only scoped reply threads for an explicit subset of stable room participant IDs through the generic Ours command catalog's `start_thread` command. Selected members receive separate root copies and reply with the SDK's native `reply_to_wire_id`; Cowork maps each descendant to the recipient's local immediate-parent copy. The selected participant-ID/CID pairs are immutable, later members are not backfilled, and excluded members receive no scoped message, file notice, notification, participant-history row, command result, or routing metadata. A message without `reply_to_wire_id` remains an ordinary whole-room message. See [Room workflow](./docs/05-room-workflow.md#scoped-reply-threads) for command discovery, schema, errors, and retry behavior, and [Messaging and history](./docs/07-messaging-history.md#reply-threading) for reply and history semantics.
27
+
26
28
  Active participants can also send files through the room identity. Cowork treats
27
29
  them as opaque bytes, archives them before consuming SDK inbox state, and relays an
28
30
  SDK-authenticated metadata envelope plus the binary file to every other active seat.
@@ -42,3 +44,4 @@ ours-cowork docs web
42
44
 
43
45
  Before production use, read the limitations topic. In particular, backups require a stopped daemon and restore uses the complete state directory.
44
46
  Lost room identity leases are recovered automatically with a non-force bind and exact persisted-CID proof. Operators can invoke the same safe path explicitly with `ours-cowork room rebind <room-id>`; it never recreates an established identity or steals a live lease.
47
+ The localhost host-management CLI/API and web Archive retain full scoped-thread visibility for administrators, including excluded-member traffic that participant APIs hide. Runtime `room.history` is an authenticated, grant-gated participant view with viewer-local cursors; host cursors and participant cursors are not interchangeable. The shared SDK history remains local to each identity and does not define Cowork routing or host archive retention.
package/dist/cli.js CHANGED
@@ -4351,11 +4351,85 @@ var SHARED_ROOM_COMMANDS = [
4351
4351
  "room.role.rest.remove"
4352
4352
  ];
4353
4353
  var RUNTIME_COMMAND_NAMES = [
4354
+ "start_thread",
4354
4355
  "list-members",
4355
4356
  "remove-member",
4356
4357
  ...SHARED_ROOM_COMMANDS
4357
4358
  ];
4358
4359
 
4360
+ // src/thread-contracts.ts
4361
+ var ParticipantIdSchema = external_exports.string().regex(
4362
+ /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4363
+ "must be a 26-character lowercase Crockford ULID"
4364
+ );
4365
+ var ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4366
+ var TopicTextSchema = external_exports.string().refine(
4367
+ (value) => Array.from(value).length >= 1 && Array.from(value).length <= 120 && value.trim().length > 0 && !/[\p{Cc}\p{Cf}]/u.test(value),
4368
+ "topic must contain 1-120 Unicode characters without control or format characters"
4369
+ );
4370
+ var InputTopicSchema = TopicTextSchema.transform((value) => value.trim());
4371
+ var StoredTopicSchema = TopicTextSchema.refine(
4372
+ (value) => value === value.trim(),
4373
+ "stored topic must already be trimmed"
4374
+ );
4375
+ var IdempotencyKeySchema = external_exports.string().regex(
4376
+ /^[A-Za-z0-9._:-]{1,128}$/,
4377
+ "must contain 1-128 portable idempotency-key characters"
4378
+ );
4379
+ var ThreadMemberSchema = external_exports.object({
4380
+ participant_id: ParticipantIdSchema,
4381
+ identity: ContainerIdSchema
4382
+ }).strict();
4383
+ var ThreadRootSchema = external_exports.object({
4384
+ schema_version: external_exports.literal(1),
4385
+ thread_id: ParticipantIdSchema,
4386
+ topic: StoredTopicSchema,
4387
+ creator_participant_id: ParticipantIdSchema,
4388
+ members: external_exports.array(ThreadMemberSchema).min(1),
4389
+ idempotency_key: IdempotencyKeySchema,
4390
+ fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase SHA-256 digest")
4391
+ }).strict().superRefine((root, context) => {
4392
+ const participantIds = /* @__PURE__ */ new Set();
4393
+ const identities = /* @__PURE__ */ new Set();
4394
+ for (const [index, member] of root.members.entries()) {
4395
+ if (participantIds.has(member.participant_id)) {
4396
+ context.addIssue({
4397
+ code: external_exports.ZodIssueCode.custom,
4398
+ path: ["members", index, "participant_id"],
4399
+ message: "thread member participant IDs must be unique"
4400
+ });
4401
+ }
4402
+ participantIds.add(member.participant_id);
4403
+ if (identities.has(member.identity)) {
4404
+ context.addIssue({
4405
+ code: external_exports.ZodIssueCode.custom,
4406
+ path: ["members", index, "identity"],
4407
+ message: "thread member identities must be unique"
4408
+ });
4409
+ }
4410
+ identities.add(member.identity);
4411
+ }
4412
+ if (!participantIds.has(root.creator_participant_id)) {
4413
+ context.addIssue({
4414
+ code: external_exports.ZodIssueCode.custom,
4415
+ path: ["creator_participant_id"],
4416
+ message: "thread creator must be a member"
4417
+ });
4418
+ }
4419
+ });
4420
+ var ThreadScopeSchema = external_exports.object({
4421
+ thread_id: ParticipantIdSchema,
4422
+ parent_key: external_exports.string().regex(
4423
+ /^(?:message|file):[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4424
+ "must identify an immediate message or file parent"
4425
+ ).optional()
4426
+ }).strict();
4427
+ var StartThreadInputSchema = external_exports.object({
4428
+ topic: InputTopicSchema,
4429
+ participant_ids: external_exports.array(ParticipantIdSchema).min(1),
4430
+ idempotency_key: IdempotencyKeySchema
4431
+ }).strict();
4432
+
4359
4433
  // src/contracts.ts
4360
4434
  var MAX_TEXT_BYTES = 262144;
4361
4435
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
@@ -4379,11 +4453,17 @@ var LowerCrockfordUlidSchema = external_exports.string().regex(
4379
4453
  /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4380
4454
  "must be a 26-character lowercase Crockford ULID"
4381
4455
  );
4382
- var ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4456
+ var ContainerIdSchema2 = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4383
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
+ ]);
4384
4464
  var RuntimeCommandGrantSchema = external_exports.object({
4385
- caller_cid: ContainerIdSchema,
4386
- command: RuntimeCommandNameSchema
4465
+ caller_cid: ContainerIdSchema2,
4466
+ command: RuntimeCommandGrantPatternSchema
4387
4467
  }).strict();
4388
4468
  function isStrictRfc3339(value) {
4389
4469
  const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
@@ -4432,7 +4512,7 @@ var RoleSchema = utf8Bounded("role", MAX_ROLE_BYTES);
4432
4512
  var ROOM_ROLE = "room";
4433
4513
  var RuntimeRoleCommandGrantSchema = external_exports.object({
4434
4514
  role: RoleSchema,
4435
- commands: external_exports.array(RuntimeCommandNameSchema).superRefine((commands, context) => {
4515
+ commands: external_exports.array(RuntimeCommandGrantPatternSchema).superRefine((commands, context) => {
4436
4516
  const seen = /* @__PURE__ */ new Set();
4437
4517
  for (const [index, command] of commands.entries()) {
4438
4518
  if (seen.has(command)) {
@@ -4727,7 +4807,7 @@ var CurrentRoomSchema = external_exports.object({
4727
4807
  lifecycle_request: external_exports.object({
4728
4808
  request_id: external_exports.string().min(1).max(256),
4729
4809
  command: external_exports.enum(["room.close", "room.delete"]),
4730
- caller_cid: ContainerIdSchema,
4810
+ caller_cid: ContainerIdSchema2,
4731
4811
  accepted_at: Rfc3339Schema,
4732
4812
  state: external_exports.enum(["pending", "failed", "completed"]),
4733
4813
  error: external_exports.literal("lifecycle_failed").optional()
@@ -4881,7 +4961,7 @@ var AcceptExternalInviteInputSchema = external_exports.object({
4881
4961
  (value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
4882
4962
  `invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
4883
4963
  ),
4884
- expected_cid: ContainerIdSchema.optional()
4964
+ expected_cid: ContainerIdSchema2.optional()
4885
4965
  }).strict();
4886
4966
  var ListMembersCommandInputSchema = external_exports.object({}).strict();
4887
4967
  var CommandIdempotencyKeySchema = external_exports.string().regex(
@@ -4956,7 +5036,9 @@ var MessageShape = {
4956
5036
  }),
4957
5037
  source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
4958
5038
  source_wire_id: NonEmptyStringSchema.optional(),
4959
- source_reply_to: ReplyReferenceSchema.optional()
5039
+ source_reply_to: ReplyReferenceSchema.optional(),
5040
+ scope: ThreadScopeSchema.optional(),
5041
+ thread_root: ThreadRootSchema.optional()
4960
5042
  };
4961
5043
  var RelayIntentShape = {
4962
5044
  kind: external_exports.literal("relay_intent"),
@@ -4964,7 +5046,12 @@ var RelayIntentShape = {
4964
5046
  file_id: LowerCrockfordUlidSchema.optional(),
4965
5047
  recipient_identity: NonEmptyStringSchema
4966
5048
  };
4967
- var RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
5049
+ var RelayResultStatusSchema = external_exports.enum([
5050
+ "queued",
5051
+ "send_failed",
5052
+ "skipped_removed",
5053
+ "skipped_reply_unavailable"
5054
+ ]);
4968
5055
  var RelayResultShape = {
4969
5056
  kind: external_exports.literal("relay_result"),
4970
5057
  intent_record_id: NonEmptyStringSchema,
@@ -5002,6 +5089,18 @@ var FileShape = {
5002
5089
  source_wire_id: NonEmptyStringSchema.optional(),
5003
5090
  source_reply_to: ReplyReferenceSchema.optional()
5004
5091
  };
5092
+ var IntakeRejectionShape = {
5093
+ kind: external_exports.literal("intake_rejection"),
5094
+ source_kind: external_exports.enum(["message", "file"]),
5095
+ source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5096
+ source_file_id: external_exports.number().int().nonnegative().safe().optional(),
5097
+ source_wire_id: NonEmptyStringSchema,
5098
+ sender_identity: NonEmptyStringSchema,
5099
+ sender_participant_id: LowerCrockfordUlidSchema,
5100
+ fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
5101
+ error: external_exports.enum(["reply_target_unavailable", "thread_files_unsupported"]),
5102
+ notification_attempt_claimed: external_exports.literal(true)
5103
+ };
5005
5104
  var MembershipIntentShape = {
5006
5105
  kind: external_exports.literal("membership_intent"),
5007
5106
  action: external_exports.enum(["remove"]),
@@ -5039,6 +5138,7 @@ var MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...Mes
5039
5138
  var FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
5040
5139
  var RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5041
5140
  var RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5141
+ var IntakeRejectionRecordSchema = external_exports.object({ ...RecordCommonShape, ...IntakeRejectionShape }).strict();
5042
5142
  var MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
5043
5143
  var MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
5044
5144
  var CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
@@ -5048,6 +5148,7 @@ var RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5048
5148
  FileRecordSchema,
5049
5149
  RelayIntentRecordSchema,
5050
5150
  RelayResultRecordSchema,
5151
+ IntakeRejectionRecordSchema,
5051
5152
  MembershipIntentRecordSchema,
5052
5153
  MembershipResultRecordSchema,
5053
5154
  CloseNoticeIntentRecordSchema,
@@ -5063,6 +5164,18 @@ function refineRelaySubject(record, context) {
5063
5164
  });
5064
5165
  }
5065
5166
  }
5167
+ function refineIntakeRejection(record, context) {
5168
+ if (record.kind !== "intake_rejection") return;
5169
+ const hasMessage = record.source_msg_id !== void 0;
5170
+ const hasFile = record.source_file_id !== void 0;
5171
+ if (hasMessage === hasFile || (record.source_kind === "message" ? !hasMessage : !hasFile)) {
5172
+ context.addIssue({
5173
+ code: external_exports.ZodIssueCode.custom,
5174
+ path: ["source_kind"],
5175
+ message: "intake rejections require exactly one numeric source field matching source_kind"
5176
+ });
5177
+ }
5178
+ }
5066
5179
  function refineFileRecord(record, context) {
5067
5180
  if (record.kind !== "file" || record.data_base64 === void 0) return;
5068
5181
  const bytes = Buffer.from(record.data_base64, "base64");
@@ -5113,6 +5226,102 @@ function refineMessageCategory(message, context) {
5113
5226
  }
5114
5227
  }
5115
5228
  }
5229
+ function refineMessageThread(message, context) {
5230
+ if (message.thread_root !== void 0) {
5231
+ if (message.scope === void 0) {
5232
+ context.addIssue({
5233
+ code: external_exports.ZodIssueCode.custom,
5234
+ path: ["scope"],
5235
+ message: "thread root messages require scope"
5236
+ });
5237
+ return;
5238
+ }
5239
+ if (message.scope.thread_id !== message.message_id || message.thread_root.thread_id !== message.message_id) {
5240
+ context.addIssue({
5241
+ code: external_exports.ZodIssueCode.custom,
5242
+ path: ["thread_root", "thread_id"],
5243
+ message: "thread root thread_id and scope thread_id must equal message_id"
5244
+ });
5245
+ }
5246
+ if (message.scope.parent_key !== void 0) {
5247
+ context.addIssue({
5248
+ code: external_exports.ZodIssueCode.custom,
5249
+ path: ["scope", "parent_key"],
5250
+ message: "parent_key is forbidden on thread root messages"
5251
+ });
5252
+ }
5253
+ if (message.category !== "chat") {
5254
+ context.addIssue({
5255
+ code: external_exports.ZodIssueCode.custom,
5256
+ path: ["category"],
5257
+ message: "thread root messages must be chat messages"
5258
+ });
5259
+ }
5260
+ if (message.text !== `Thread: ${message.thread_root.topic}`) {
5261
+ context.addIssue({
5262
+ code: external_exports.ZodIssueCode.custom,
5263
+ path: ["text"],
5264
+ message: "thread root message text must identify its topic"
5265
+ });
5266
+ }
5267
+ const creator = message.thread_root.members.find(
5268
+ (member) => member.participant_id === message.thread_root?.creator_participant_id
5269
+ );
5270
+ if (creator?.identity !== message.author.identity) {
5271
+ context.addIssue({
5272
+ code: external_exports.ZodIssueCode.custom,
5273
+ path: ["thread_root", "creator_participant_id"],
5274
+ message: "thread root creator must match the message author"
5275
+ });
5276
+ }
5277
+ if (message.author_alias !== void 0 && message.author_alias.participant_id !== message.thread_root.creator_participant_id) {
5278
+ context.addIssue({
5279
+ code: external_exports.ZodIssueCode.custom,
5280
+ path: ["author_alias", "participant_id"],
5281
+ message: "thread root author alias must identify the creator"
5282
+ });
5283
+ }
5284
+ for (const field of ["source_msg_id", "source_wire_id", "source_reply_to"]) {
5285
+ if (message[field] !== void 0) {
5286
+ context.addIssue({
5287
+ code: external_exports.ZodIssueCode.custom,
5288
+ path: [field],
5289
+ message: `${field} is forbidden on thread root messages`
5290
+ });
5291
+ }
5292
+ }
5293
+ return;
5294
+ }
5295
+ if (message.scope === void 0) return;
5296
+ if (message.scope.parent_key === void 0) {
5297
+ context.addIssue({
5298
+ code: external_exports.ZodIssueCode.custom,
5299
+ path: ["scope", "parent_key"],
5300
+ message: "thread descendants require an immediate parent_key"
5301
+ });
5302
+ }
5303
+ if (message.scope.thread_id === message.message_id) {
5304
+ context.addIssue({
5305
+ code: external_exports.ZodIssueCode.custom,
5306
+ path: ["scope", "thread_id"],
5307
+ message: "thread descendant thread_id must identify a distinct root message"
5308
+ });
5309
+ }
5310
+ if (message.source_reply_to === void 0) {
5311
+ context.addIssue({
5312
+ code: external_exports.ZodIssueCode.custom,
5313
+ path: ["source_reply_to"],
5314
+ message: "thread descendants require source_reply_to"
5315
+ });
5316
+ }
5317
+ if (message.category !== "chat") {
5318
+ context.addIssue({
5319
+ code: external_exports.ZodIssueCode.custom,
5320
+ path: ["category"],
5321
+ message: "thread descendants must be chat messages"
5322
+ });
5323
+ }
5324
+ }
5116
5325
  var CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5117
5326
  if (record.record_id !== `${record.room_id}:${record.seq}`) {
5118
5327
  context.addIssue({
@@ -5121,8 +5330,12 @@ var CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record
5121
5330
  message: 'record_id must equal room_id + ":" + seq'
5122
5331
  });
5123
5332
  }
5124
- if (record.kind === "message") refineMessageCategory(record, context);
5333
+ if (record.kind === "message") {
5334
+ refineMessageCategory(record, context);
5335
+ refineMessageThread(record, context);
5336
+ }
5125
5337
  refineRelaySubject(record, context);
5338
+ refineIntakeRejection(record, context);
5126
5339
  refineFileRecord(record, context);
5127
5340
  });
5128
5341
  var AppendRecordSchema = external_exports.discriminatedUnion("kind", [
@@ -5130,11 +5343,16 @@ var AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5130
5343
  external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
5131
5344
  external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5132
5345
  external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5346
+ external_exports.object({ ...AppendCommonShape, ...IntakeRejectionShape }).strict(),
5133
5347
  external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
5134
5348
  external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
5135
5349
  ]).superRefine((record, context) => {
5136
- if (record.kind === "message") refineMessageCategory(record, context);
5350
+ if (record.kind === "message") {
5351
+ refineMessageCategory(record, context);
5352
+ refineMessageThread(record, context);
5353
+ }
5137
5354
  refineRelaySubject(record, context);
5355
+ refineIntakeRejection(record, context);
5138
5356
  refineFileRecord(record, context);
5139
5357
  });
5140
5358
 
@@ -5431,8 +5649,8 @@ async function roomRequest(command, args) {
5431
5649
  const role = requiredFlag(parsed, "--role", "room role-command-set");
5432
5650
  const value = requiredFlag(parsed, "--commands", "room role-command-set");
5433
5651
  const commands = value === "none" ? [] : value.split(",");
5434
- if (new Set(commands).size !== commands.length || commands.some((item) => !RuntimeCommandNameSchema.safeParse(item).success)) {
5435
- usageError("--commands must be a unique comma-separated list of built-in command names or none");
5652
+ if (new Set(commands).size !== commands.length || commands.some((item) => !RuntimeCommandGrantPatternSchema.safeParse(item).success)) {
5653
+ usageError("--commands must be a unique comma-separated list of command names, * or namespace.* patterns, or none");
5436
5654
  }
5437
5655
  return { method: "room.command.role.set", params: { room_id: roomId, role, commands } };
5438
5656
  }
@@ -5443,8 +5661,8 @@ async function roomRequest(command, args) {
5443
5661
  3,
5444
5662
  `room ${command}`
5445
5663
  );
5446
- if (!RuntimeCommandNameSchema.safeParse(runtimeCommand).success) {
5447
- usageError("runtime command must be a supported built-in command name");
5664
+ if (!RuntimeCommandGrantPatternSchema.safeParse(runtimeCommand).success) {
5665
+ usageError("runtime command must be a supported command name, * or namespace.* pattern");
5448
5666
  }
5449
5667
  return {
5450
5668
  method: command === "command-grant" ? "room.command.grant" : "room.command.revoke",