@ours.network/cowork 0.4.0 → 0.4.1-nightly.20260816.4aaf940
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 +7 -7
- package/dist/cli.js +112 -10
- package/dist/daemon.js +988 -2226
- package/docs/01-prerequisites.md +1 -1
- package/docs/02-installation.md +1 -1
- package/docs/03-configuration.md +31 -1
- package/docs/04-daemon-lifecycle.md +4 -0
- package/docs/05-room-workflow.md +2 -2
- package/docs/06-invites.md +2 -0
- package/docs/07-messaging-history.md +2 -2
- package/docs/08-backup-restore.md +2 -2
- package/docs/09-service-management.md +4 -2
- package/docs/10-limitations.md +3 -2
- package/docs/11-web-console.md +1 -12
- package/package.json +3 -6
- package/dist/mufl_code/BBAE58CF78DEE59692F456EAFFA9A6109835B66846FC3989F6D201B8F4523A55.muflo +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ours-cowork
|
|
2
2
|
|
|
3
|
-
`ours-cowork` is a standalone daemon for durable ours mission rooms. It owns one ordinary
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
23
|
-
them as opaque bytes, archives them before consuming
|
|
24
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
29
29
|
|
|
30
30
|
Licensed under [FSL-1.1-Apache-2.0](./LICENSE).
|
|
31
31
|
|
package/dist/cli.js
CHANGED
|
@@ -4065,6 +4065,33 @@ 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
|
+
});
|
|
4068
4095
|
var CoworkConfigSchema = external_exports.object({
|
|
4069
4096
|
version: external_exports.literal(1),
|
|
4070
4097
|
brokerUrl: external_exports.string().url().refine((value) => {
|
|
@@ -4075,7 +4102,10 @@ var CoworkConfigSchema = external_exports.object({
|
|
|
4075
4102
|
rest: external_exports.object({
|
|
4076
4103
|
enabled: external_exports.boolean(),
|
|
4077
4104
|
port: external_exports.number().int().min(1).max(65535)
|
|
4078
|
-
}).strict()
|
|
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()
|
|
4079
4109
|
}).strict();
|
|
4080
4110
|
var CoworkConfigError = class extends Error {
|
|
4081
4111
|
constructor(message, options) {
|
|
@@ -4114,6 +4144,7 @@ function loadConfig(env = process.env, io = {}) {
|
|
|
4114
4144
|
throw new CoworkConfigError(`configured cowork config does not exist: ${configPath}`);
|
|
4115
4145
|
}
|
|
4116
4146
|
const restPort = env.OURS_COWORK_REST_PORT === void 0 ? void 0 : parsePort(env.OURS_COWORK_REST_PORT);
|
|
4147
|
+
const daemon = mergeDaemonSelection(env, file.daemon);
|
|
4117
4148
|
try {
|
|
4118
4149
|
return CoworkConfigSchema.parse({
|
|
4119
4150
|
version: 1,
|
|
@@ -4122,12 +4153,69 @@ function loadConfig(env = process.env, io = {}) {
|
|
|
4122
4153
|
rest: {
|
|
4123
4154
|
enabled: restPort === void 0 ? file.rest.enabled : true,
|
|
4124
4155
|
port: restPort ?? file.rest.port
|
|
4125
|
-
}
|
|
4156
|
+
},
|
|
4157
|
+
...daemon === void 0 ? {} : { daemon }
|
|
4126
4158
|
});
|
|
4127
4159
|
} catch (error) {
|
|
4128
4160
|
throw new CoworkConfigError("invalid effective cowork config", { cause: error });
|
|
4129
4161
|
}
|
|
4130
4162
|
}
|
|
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
|
+
}
|
|
4131
4219
|
function ensureRuntimeState(config, io = {}) {
|
|
4132
4220
|
const parsed = CoworkConfigSchema.parse(config);
|
|
4133
4221
|
const fs2 = io.fs ?? nodeFs;
|
|
@@ -4328,9 +4416,9 @@ var RoomNameSchema = external_exports.string().refine(
|
|
|
4328
4416
|
});
|
|
4329
4417
|
}
|
|
4330
4418
|
});
|
|
4331
|
-
var ROOM_IDENTITY_PREFIX = "ours-cowork-
|
|
4332
|
-
function roomIdentityName(
|
|
4333
|
-
return `${ROOM_IDENTITY_PREFIX}${
|
|
4419
|
+
var ROOM_IDENTITY_PREFIX = "ours-cowork-";
|
|
4420
|
+
function roomIdentityName(roomId) {
|
|
4421
|
+
return `${ROOM_IDENTITY_PREFIX}${LowerCrockfordUlidSchema.parse(roomId)}`;
|
|
4334
4422
|
}
|
|
4335
4423
|
function legacyRoomIdentityName(roomId) {
|
|
4336
4424
|
return `cowork-room-${LowerCrockfordUlidSchema.parse(roomId)}`;
|
|
@@ -4510,7 +4598,7 @@ var RoleBriefingSchema = external_exports.object({
|
|
|
4510
4598
|
}).strict();
|
|
4511
4599
|
function refineRoomLineage(room, context) {
|
|
4512
4600
|
const pendingIdentityName = legacyRoomIdentityName(room.room_id);
|
|
4513
|
-
const currentPendingIdentityName =
|
|
4601
|
+
const currentPendingIdentityName = roomIdentityName(room.room_id);
|
|
4514
4602
|
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;
|
|
4515
4603
|
if (room.identity_cid === "" && !exactPacketPending) {
|
|
4516
4604
|
context.addIssue({
|
|
@@ -4738,6 +4826,10 @@ var AuthorAliasSchema = external_exports.object({
|
|
|
4738
4826
|
participant_id: LowerCrockfordUlidSchema,
|
|
4739
4827
|
alias: NonEmptyStringSchema
|
|
4740
4828
|
}).strict();
|
|
4829
|
+
var ReplyReferenceSchema = external_exports.object({
|
|
4830
|
+
wire_id: NonEmptyStringSchema,
|
|
4831
|
+
sentence: PositiveSafeIntegerSchema.optional()
|
|
4832
|
+
}).strict();
|
|
4741
4833
|
var MessageShape = {
|
|
4742
4834
|
kind: external_exports.literal("message"),
|
|
4743
4835
|
message_id: LowerCrockfordUlidSchema,
|
|
@@ -4762,7 +4854,8 @@ var MessageShape = {
|
|
|
4762
4854
|
}
|
|
4763
4855
|
}),
|
|
4764
4856
|
source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
|
|
4765
|
-
source_wire_id: NonEmptyStringSchema.optional()
|
|
4857
|
+
source_wire_id: NonEmptyStringSchema.optional(),
|
|
4858
|
+
source_reply_to: ReplyReferenceSchema.optional()
|
|
4766
4859
|
};
|
|
4767
4860
|
var RelayIntentShape = {
|
|
4768
4861
|
kind: external_exports.literal("relay_intent"),
|
|
@@ -4805,7 +4898,8 @@ var FileShape = {
|
|
|
4805
4898
|
}
|
|
4806
4899
|
}),
|
|
4807
4900
|
source_file_id: external_exports.number().int().nonnegative().safe(),
|
|
4808
|
-
source_wire_id: NonEmptyStringSchema.optional()
|
|
4901
|
+
source_wire_id: NonEmptyStringSchema.optional(),
|
|
4902
|
+
source_reply_to: ReplyReferenceSchema.optional()
|
|
4809
4903
|
};
|
|
4810
4904
|
var MembershipIntentShape = {
|
|
4811
4905
|
kind: external_exports.literal("membership_intent"),
|
|
@@ -5620,7 +5714,14 @@ function serviceEnvironment(config) {
|
|
|
5620
5714
|
return {
|
|
5621
5715
|
OURS_COWORK_BROKER_URL: config.brokerUrl,
|
|
5622
5716
|
OURS_COWORK_STATE_DIR: config.stateDir,
|
|
5623
|
-
...config.rest.enabled ? { OURS_COWORK_REST_PORT: String(config.rest.port) } : {}
|
|
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
|
+
}
|
|
5624
5725
|
};
|
|
5625
5726
|
}
|
|
5626
5727
|
function validateServiceConfiguration(config) {
|
|
@@ -5662,13 +5763,14 @@ function installSystemd(config) {
|
|
|
5662
5763
|
Description=ours-cowork standalone mission-room daemon
|
|
5663
5764
|
After=network-online.target
|
|
5664
5765
|
Wants=network-online.target
|
|
5766
|
+
StartLimitIntervalSec=0
|
|
5665
5767
|
|
|
5666
5768
|
[Service]
|
|
5667
5769
|
Type=simple
|
|
5668
5770
|
ExecStart=${systemdExecutableQuote(SELF)} serve
|
|
5669
5771
|
${environment}
|
|
5670
5772
|
Restart=on-failure
|
|
5671
|
-
RestartSec=
|
|
5773
|
+
RestartSec=5
|
|
5672
5774
|
|
|
5673
5775
|
[Install]
|
|
5674
5776
|
WantedBy=default.target
|