@ours.network/cowork 0.4.1-nightly.20260816.4aaf940 → 0.5.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
@@ -1,6 +1,6 @@
1
1
  # ours-cowork
2
2
 
3
- `ours-cowork` is a standalone standard-SDK daemon for durable ours mission rooms. It owns one ordinary ours identity per room through an embedded `@ours.network/sdk` runtime by default, an ordered local archive, a private Unix management socket, and a localhost web console with room RPC.
3
+ `ours-cowork` is a standalone daemon for durable ours mission rooms. It owns one ordinary packet per room, an ordered local archive, a private Unix management socket, and a localhost web console with room RPC.
4
4
 
5
5
  ```sh
6
6
  ours-cowork web
@@ -9,23 +9,23 @@ ours-cowork docs
9
9
 
10
10
  `ours-cowork web` starts the daemon if it is absent, waits for the console, and opens `http://127.0.0.1:3052/`. Create a room with a friendly display name first, then add each invitation requirement from its Invite panel. Names are trimmed, normalized to Unicode NFC, and may contain 1–64 Unicode characters excluding control and format characters. Duplicate names are allowed. The Communication view contains the human-readable room chat; operational records remain in Events and the complete ordered stream remains in Archive.
11
11
 
12
- The friendly `room_name` is presentation metadata. New rooms use the globally unique SDK identity `ours-cowork-<room_id>`; that authenticated name is independent of later display-name changes. Duplicate display names are allowed because identity CIDs and room IDs, not names, are the authorization and routing keys. Pre-1.0 rooms using `cowork-room-<room_id>` or `ours-cowork-room:<name>` custom packet state are detected and refused: back them up with the old release, recreate them, and re-invite their participants.
12
+ The friendly `room_name` is presentation metadata. New rooms announce the technical identity `ours-cowork-room:<initial room_name>`; that authenticated announced name is frozen when the display name later changes. Duplicate display and announced names are allowed because identity CIDs, not names, are the authorization and routing keys. The opaque `room_id` remains the stable key for URLs, storage, and identity correlation. Existing `cowork-room-<room_id>` identities are never renamed or recreated, and rooms created before friendly names existed migrate only their display metadata to `Room <first 8 room_id characters>`.
13
13
 
14
14
  The localhost HTTP console has no authentication. Keep it bound to `127.0.0.1`; do not proxy, forward, or expose the port to other hosts. Room state is refreshed by periodic polling, not pushed to the browser.
15
15
 
16
- By default the runtime is embedded and no configuration changes. A `daemon` block with `mode: "external"` instead hosts the room identities on an ours daemon you already run the common one, or a dedicated one on its own port and state directory. See the configuration topic.
16
+ The same listener describes its own room management REST API: `http://127.0.0.1:3052/openapi.json` is the OpenAPI 3.1 document and `http://127.0.0.1:3052/docs` is the browser UI for it. Both are read-only, load no remote assets, and follow the console's loopback-only exposure rules.
17
17
 
18
18
  The package runs independently with its own config and state directory. Operator room commands use one JSONL request over `management.sock`. Use `--json` for automation; its stdout is a single JSON value and diagnostics are included in that value.
19
19
 
20
20
  Ordinary ours-mcp identities can join only as remote participants over the ours protocol.
21
21
 
22
- Active participants can also send files through the room identity. Cowork treats
23
- them as opaque bytes, archives them before consuming SDK inbox state, and relays an
24
- SDK-authenticated metadata envelope plus the binary file to every other active seat.
22
+ Active participants can also send files through the room packet. Cowork treats
23
+ them as opaque bytes, archives them before consuming packet state, and relays a
24
+ signed metadata envelope plus the core binary file to every other active seat.
25
25
  Files are limited to 2 MiB; larger inputs fail loudly instead of weakening
26
26
  crash recovery guarantees.
27
27
 
28
- The release package contains the standalone daemon, operator CLI, deterministic web assets, and all eleven offline operator topics. Standard identity, messaging, reply, file, and lifecycle behavior comes from the public `@ours.network/sdk`; no custom MUFL room actor is shipped.
28
+ The release package contains the standalone daemon, operator CLI, compiled room packet, deterministic web assets, and all eleven offline operator topics. It has no dependency on another agent daemon.
29
29
 
30
30
  Licensed under [FSL-1.1-Apache-2.0](./LICENSE).
31
31
 
package/dist/cli.js CHANGED
@@ -4065,33 +4065,6 @@ var NEVER = INVALID;
4065
4065
  var DIRECTORY_MODE = 448;
4066
4066
  var FILE_MODE = 384;
4067
4067
  var NO_FOLLOW = nodeFs.constants.O_NOFOLLOW ?? 0;
4068
- var CoworkDaemonSchema = external_exports.object({
4069
- mode: external_exports.enum(["embedded", "external"]),
4070
- endpoint: external_exports.string().url().refine(isDaemonOrigin, "daemon.endpoint must be an http(s) origin with no credentials, path, query, or fragment").refine(isConfidentialDaemonEndpoint, "daemon.endpoint must use https:// unless the daemon is on this host (http:// is allowed only for localhost, 127.0.0.0/8, or [::1])").transform(normalizeDaemonEndpoint).optional(),
4071
- stateDir: external_exports.string().min(1).transform((value) => resolve(value)).optional()
4072
- }).strict().superRefine((value, ctx) => {
4073
- if (value.mode === "external") {
4074
- for (const field of ["endpoint", "stateDir"]) {
4075
- if (value[field] === void 0) {
4076
- ctx.addIssue({
4077
- code: external_exports.ZodIssueCode.custom,
4078
- path: [field],
4079
- message: `daemon.${field} is required when daemon.mode is "external"`
4080
- });
4081
- }
4082
- }
4083
- return;
4084
- }
4085
- for (const field of ["endpoint", "stateDir"]) {
4086
- if (value[field] !== void 0) {
4087
- ctx.addIssue({
4088
- code: external_exports.ZodIssueCode.custom,
4089
- path: [field],
4090
- message: `daemon.${field} is only valid when daemon.mode is "external"`
4091
- });
4092
- }
4093
- }
4094
- });
4095
4068
  var CoworkConfigSchema = external_exports.object({
4096
4069
  version: external_exports.literal(1),
4097
4070
  brokerUrl: external_exports.string().url().refine((value) => {
@@ -4102,10 +4075,7 @@ var CoworkConfigSchema = external_exports.object({
4102
4075
  rest: external_exports.object({
4103
4076
  enabled: external_exports.boolean(),
4104
4077
  port: external_exports.number().int().min(1).max(65535)
4105
- }).strict(),
4106
- // Absent unless an operator selected a daemon explicitly, so an untouched
4107
- // deployment keeps producing byte-identical effective configuration.
4108
- daemon: CoworkDaemonSchema.optional()
4078
+ }).strict()
4109
4079
  }).strict();
4110
4080
  var CoworkConfigError = class extends Error {
4111
4081
  constructor(message, options) {
@@ -4144,7 +4114,6 @@ function loadConfig(env = process.env, io = {}) {
4144
4114
  throw new CoworkConfigError(`configured cowork config does not exist: ${configPath}`);
4145
4115
  }
4146
4116
  const restPort = env.OURS_COWORK_REST_PORT === void 0 ? void 0 : parsePort(env.OURS_COWORK_REST_PORT);
4147
- const daemon = mergeDaemonSelection(env, file.daemon);
4148
4117
  try {
4149
4118
  return CoworkConfigSchema.parse({
4150
4119
  version: 1,
@@ -4153,69 +4122,12 @@ function loadConfig(env = process.env, io = {}) {
4153
4122
  rest: {
4154
4123
  enabled: restPort === void 0 ? file.rest.enabled : true,
4155
4124
  port: restPort ?? file.rest.port
4156
- },
4157
- ...daemon === void 0 ? {} : { daemon }
4125
+ }
4158
4126
  });
4159
4127
  } catch (error) {
4160
4128
  throw new CoworkConfigError("invalid effective cowork config", { cause: error });
4161
4129
  }
4162
4130
  }
4163
- function mergeDaemonSelection(env, file) {
4164
- const mode = env.OURS_COWORK_DAEMON_MODE;
4165
- const endpoint = env.OURS_COWORK_DAEMON_ENDPOINT;
4166
- const stateDir = env.OURS_COWORK_DAEMON_STATE_DIR;
4167
- if (mode === void 0 && endpoint === void 0 && stateDir === void 0) return file;
4168
- if (mode !== void 0 && mode !== "embedded" && mode !== "external") {
4169
- throw new CoworkConfigError('OURS_COWORK_DAEMON_MODE must be "embedded" or "external"');
4170
- }
4171
- const effectiveMode = mode ?? file?.mode ?? "external";
4172
- if (effectiveMode === "embedded") {
4173
- if (endpoint !== void 0 || stateDir !== void 0) {
4174
- throw new CoworkConfigError(
4175
- 'OURS_COWORK_DAEMON_ENDPOINT and OURS_COWORK_DAEMON_STATE_DIR require daemon mode "external"'
4176
- );
4177
- }
4178
- return { mode: "embedded" };
4179
- }
4180
- const effectiveEndpoint = endpoint ?? file?.endpoint;
4181
- const effectiveStateDir = stateDir ?? file?.stateDir;
4182
- return {
4183
- mode: "external",
4184
- ...effectiveEndpoint === void 0 ? {} : { endpoint: effectiveEndpoint },
4185
- ...effectiveStateDir === void 0 ? {} : { stateDir: effectiveStateDir }
4186
- };
4187
- }
4188
- function isDaemonOrigin(value) {
4189
- let url;
4190
- try {
4191
- url = new URL(value);
4192
- } catch {
4193
- return false;
4194
- }
4195
- return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.search === "" && url.hash === "" && (url.pathname === "" || url.pathname === "/");
4196
- }
4197
- function isConfidentialDaemonEndpoint(value) {
4198
- let url;
4199
- try {
4200
- url = new URL(value);
4201
- } catch {
4202
- return false;
4203
- }
4204
- return url.protocol === "https:" || isLoopbackHost(url.hostname);
4205
- }
4206
- function isLoopbackHost(hostname) {
4207
- const host = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
4208
- if (host === "localhost") return true;
4209
- if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
4210
- const octets = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
4211
- if (!octets) return false;
4212
- const parts = octets.slice(1).map(Number);
4213
- return parts.every((part) => part <= 255) && parts[0] === 127;
4214
- }
4215
- function normalizeDaemonEndpoint(value) {
4216
- const url = new URL(value);
4217
- return `${url.protocol}//${url.host}`;
4218
- }
4219
4131
  function ensureRuntimeState(config, io = {}) {
4220
4132
  const parsed = CoworkConfigSchema.parse(config);
4221
4133
  const fs2 = io.fs ?? nodeFs;
@@ -4416,9 +4328,9 @@ var RoomNameSchema = external_exports.string().refine(
4416
4328
  });
4417
4329
  }
4418
4330
  });
4419
- var ROOM_IDENTITY_PREFIX = "ours-cowork-";
4420
- function roomIdentityName(roomId) {
4421
- return `${ROOM_IDENTITY_PREFIX}${LowerCrockfordUlidSchema.parse(roomId)}`;
4331
+ var ROOM_IDENTITY_PREFIX = "ours-cowork-room:";
4332
+ function roomIdentityName(roomName) {
4333
+ return `${ROOM_IDENTITY_PREFIX}${RoomNameSchema.parse(roomName)}`;
4422
4334
  }
4423
4335
  function legacyRoomIdentityName(roomId) {
4424
4336
  return `cowork-room-${LowerCrockfordUlidSchema.parse(roomId)}`;
@@ -4598,7 +4510,7 @@ var RoleBriefingSchema = external_exports.object({
4598
4510
  }).strict();
4599
4511
  function refineRoomLineage(room, context) {
4600
4512
  const pendingIdentityName = legacyRoomIdentityName(room.room_id);
4601
- const currentPendingIdentityName = roomIdentityName(room.room_id);
4513
+ const currentPendingIdentityName = room.room_name === void 0 ? void 0 : roomIdentityName(room.room_name);
4602
4514
  const exactPacketPending = room.state === "provisioning" && room.status === "packet_pending" && (room.identity_name === pendingIdentityName || room.identity_name === currentPendingIdentityName) && room.invites.length === 0 && room.seats.length === 0 && room.activated_at === void 0 && room.closed_at === void 0;
4603
4515
  if (room.identity_cid === "" && !exactPacketPending) {
4604
4516
  context.addIssue({
@@ -4669,6 +4581,13 @@ var CurrentRoomSchema = external_exports.object({
4669
4581
  version: external_exports.literal(2),
4670
4582
  mission: MissionSchema,
4671
4583
  role_briefings: external_exports.record(RoleSchema, RoleBriefingSchema),
4584
+ /**
4585
+ * Roles a REST caller may author under (spec §3.1). A plain array of names:
4586
+ * the role name IS the identifier, so there is nothing per-role to store.
4587
+ * Not a seat, not a membership — no invariant here constrains it, because a
4588
+ * role is a label and several speakers may share one.
4589
+ */
4590
+ rest_roles: external_exports.array(RoleSchema),
4672
4591
  anonymous: external_exports.boolean(),
4673
4592
  quiet_membership: external_exports.boolean(),
4674
4593
  membership_epoch: external_exports.number().int().nonnegative().safe(),
@@ -4760,10 +4679,17 @@ function defaultRoomName(roomId) {
4760
4679
  return `Room ${LowerCrockfordUlidSchema.parse(roomId).slice(0, 8)}`;
4761
4680
  }
4762
4681
  var RoomSchema = external_exports.preprocess((value) => {
4763
- if (typeof value !== "object" || value === null || Object.hasOwn(value, "room_name")) return value;
4764
- const roomId = value.room_id;
4765
- if (typeof roomId !== "string" || !LowerCrockfordUlidSchema.safeParse(roomId).success) return value;
4766
- return { ...value, room_name: defaultRoomName(roomId) };
4682
+ if (typeof value !== "object" || value === null) return value;
4683
+ const patch = {};
4684
+ if (!Object.hasOwn(value, "room_name")) {
4685
+ const roomId = value.room_id;
4686
+ if (typeof roomId === "string" && LowerCrockfordUlidSchema.safeParse(roomId).success) {
4687
+ patch.room_name = defaultRoomName(roomId);
4688
+ }
4689
+ }
4690
+ if (!Object.hasOwn(value, "rest_roles")) patch.rest_roles = [];
4691
+ if (Object.keys(patch).length === 0) return value;
4692
+ return { ...value, ...patch };
4767
4693
  }, CurrentRoomSchema);
4768
4694
  var CreateRoomInputSchema = external_exports.object({
4769
4695
  name: RoomNameSchema.optional(),
@@ -4789,6 +4715,13 @@ var RoleBriefingDeleteInputSchema = external_exports.object({
4789
4715
  var PostMessageInputSchema = external_exports.object({
4790
4716
  text: MessageTextSchema
4791
4717
  }).strict();
4718
+ var PostAsRoleInputSchema = external_exports.object({
4719
+ role: RoleSchema,
4720
+ text: MessageTextSchema
4721
+ }).strict();
4722
+ var RestRoleInputSchema = external_exports.object({
4723
+ role: RoleSchema
4724
+ }).strict();
4792
4725
  var ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4793
4726
  var AcceptExternalInviteInputSchema = external_exports.object({
4794
4727
  role: RoleSchema,
@@ -4826,10 +4759,6 @@ var AuthorAliasSchema = external_exports.object({
4826
4759
  participant_id: LowerCrockfordUlidSchema,
4827
4760
  alias: NonEmptyStringSchema
4828
4761
  }).strict();
4829
- var ReplyReferenceSchema = external_exports.object({
4830
- wire_id: NonEmptyStringSchema,
4831
- sentence: PositiveSafeIntegerSchema.optional()
4832
- }).strict();
4833
4762
  var MessageShape = {
4834
4763
  kind: external_exports.literal("message"),
4835
4764
  message_id: LowerCrockfordUlidSchema,
@@ -4854,8 +4783,7 @@ var MessageShape = {
4854
4783
  }
4855
4784
  }),
4856
4785
  source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
4857
- source_wire_id: NonEmptyStringSchema.optional(),
4858
- source_reply_to: ReplyReferenceSchema.optional()
4786
+ source_wire_id: NonEmptyStringSchema.optional()
4859
4787
  };
4860
4788
  var RelayIntentShape = {
4861
4789
  kind: external_exports.literal("relay_intent"),
@@ -4898,8 +4826,7 @@ var FileShape = {
4898
4826
  }
4899
4827
  }),
4900
4828
  source_file_id: external_exports.number().int().nonnegative().safe(),
4901
- source_wire_id: NonEmptyStringSchema.optional(),
4902
- source_reply_to: ReplyReferenceSchema.optional()
4829
+ source_wire_id: NonEmptyStringSchema.optional()
4903
4830
  };
4904
4831
  var MembershipIntentShape = {
4905
4832
  kind: external_exports.literal("membership_intent"),
@@ -5131,6 +5058,8 @@ Room commands:
5131
5058
  participants <room-id>
5132
5059
  history <room-id> [--after <seq>] [--limit <n>] [--view operator|participant]
5133
5060
  message <room-id> --text <text>
5061
+ say <room-id> --role <label> --text <text>
5062
+ rest-role <room-id> --role <label> [--remove]
5134
5063
  close <room-id>
5135
5064
  delete <room-id> --yes
5136
5065
  recover <room-id> [--confirm <old-invite-id> <new-invite-id>]
@@ -5342,6 +5271,22 @@ async function roomRequest(command, args) {
5342
5271
  text: requiredFlag(parsed, "--text", "room message")
5343
5272
  } };
5344
5273
  }
5274
+ case "say": {
5275
+ const parsed = parseOptions(args, ["--role", "--text"]);
5276
+ const [roomId] = exactPositionals(parsed, 1, "room say");
5277
+ return { method: "room.say", params: {
5278
+ room_id: roomId,
5279
+ role: requiredFlag(parsed, "--role", "room say"),
5280
+ text: requiredFlag(parsed, "--text", "room say")
5281
+ } };
5282
+ }
5283
+ case "rest-role": {
5284
+ const parsed = parseOptions(args, ["--role"], ["--remove"]);
5285
+ const [roomId] = exactPositionals(parsed, 1, "room rest-role");
5286
+ const role = requiredFlag(parsed, "--role", "room rest-role");
5287
+ const method = parsed.booleans.has("--remove") ? "room.role.rest.remove" : "room.role.rest.add";
5288
+ return { method, params: { room_id: roomId, role } };
5289
+ }
5345
5290
  case "delete": {
5346
5291
  const parsed = parseOptions(args, [], ["--yes"]);
5347
5292
  const [roomId] = exactPositionals(parsed, 1, "room delete");
@@ -5714,14 +5659,7 @@ function serviceEnvironment(config) {
5714
5659
  return {
5715
5660
  OURS_COWORK_BROKER_URL: config.brokerUrl,
5716
5661
  OURS_COWORK_STATE_DIR: config.stateDir,
5717
- ...config.rest.enabled ? { OURS_COWORK_REST_PORT: String(config.rest.port) } : {},
5718
- // Endpoint and state directory only. The daemon's API token stays where the
5719
- // SDK finds it and never reaches a service definition.
5720
- ...config.daemon === void 0 ? {} : {
5721
- OURS_COWORK_DAEMON_MODE: config.daemon.mode,
5722
- ...config.daemon.endpoint === void 0 ? {} : { OURS_COWORK_DAEMON_ENDPOINT: config.daemon.endpoint },
5723
- ...config.daemon.stateDir === void 0 ? {} : { OURS_COWORK_DAEMON_STATE_DIR: config.daemon.stateDir }
5724
- }
5662
+ ...config.rest.enabled ? { OURS_COWORK_REST_PORT: String(config.rest.port) } : {}
5725
5663
  };
5726
5664
  }
5727
5665
  function validateServiceConfiguration(config) {
@@ -5763,14 +5701,13 @@ function installSystemd(config) {
5763
5701
  Description=ours-cowork standalone mission-room daemon
5764
5702
  After=network-online.target
5765
5703
  Wants=network-online.target
5766
- StartLimitIntervalSec=0
5767
5704
 
5768
5705
  [Service]
5769
5706
  Type=simple
5770
5707
  ExecStart=${systemdExecutableQuote(SELF)} serve
5771
5708
  ${environment}
5772
5709
  Restart=on-failure
5773
- RestartSec=5
5710
+ RestartSec=2
5774
5711
 
5775
5712
  [Install]
5776
5713
  WantedBy=default.target