@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 +7 -7
- package/dist/cli.js +53 -116
- package/dist/daemon.js +2410 -1021
- package/dist/mufl_code/BBAE58CF78DEE59692F456EAFFA9A6109835B66846FC3989F6D201B8F4523A55.muflo +0 -0
- package/dist/web/assets/app.js +3 -3
- package/docs/01-prerequisites.md +1 -1
- package/docs/02-installation.md +1 -1
- package/docs/03-configuration.md +1 -31
- package/docs/04-daemon-lifecycle.md +0 -4
- package/docs/05-room-workflow.md +2 -2
- package/docs/06-invites.md +0 -2
- package/docs/07-messaging-history.md +22 -2
- package/docs/08-backup-restore.md +2 -2
- package/docs/09-service-management.md +2 -4
- package/docs/10-limitations.md +2 -3
- package/docs/11-web-console.md +12 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ours-cowork
|
|
2
2
|
|
|
3
|
-
`ours-cowork` is a standalone
|
|
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
|
|
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
|
-
|
|
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
|
|
23
|
-
them as opaque bytes, archives them before consuming
|
|
24
|
-
|
|
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.
|
|
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(
|
|
4421
|
-
return `${ROOM_IDENTITY_PREFIX}${
|
|
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.
|
|
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
|
|
4764
|
-
const
|
|
4765
|
-
if (
|
|
4766
|
-
|
|
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=
|
|
5710
|
+
RestartSec=2
|
|
5774
5711
|
|
|
5775
5712
|
[Install]
|
|
5776
5713
|
WantedBy=default.target
|