@nordbyte/nordrelay 0.4.1 → 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.
@@ -1,11 +1,68 @@
1
- const ALL_PERMISSIONS = [
1
+ import { permissionForWebRequestFromContract } from "./web-api-contract.js";
2
+ export const ALL_PERMISSIONS = [
2
3
  "inspect",
3
- "sessions",
4
- "prompt",
5
- "files",
6
- "settings",
7
- "auth",
8
- "admin",
4
+ "sessions.read",
5
+ "sessions.write",
6
+ "prompt.send",
7
+ "prompt.abort",
8
+ "files.read",
9
+ "files.write",
10
+ "settings.read",
11
+ "settings.write",
12
+ "auth.manage",
13
+ "diagnostics.read",
14
+ "logs.read",
15
+ "logs.clear",
16
+ "queue.read",
17
+ "queue.write",
18
+ "updates.run",
19
+ "system.restart",
20
+ "users.read",
21
+ "users.write",
22
+ "audit.read",
23
+ ];
24
+ export const ADMIN_GROUP_ID = "admin";
25
+ export const USER_GROUP_ID = "user";
26
+ export const READONLY_GROUP_ID = "readonly";
27
+ export const BUILTIN_GROUPS = [
28
+ {
29
+ id: ADMIN_GROUP_ID,
30
+ name: "Admin",
31
+ description: "Full access to every NordRelay feature, user management, updates, and system controls.",
32
+ permissions: ALL_PERMISSIONS,
33
+ system: true,
34
+ },
35
+ {
36
+ id: USER_GROUP_ID,
37
+ name: "User",
38
+ description: "Normal read/write use of agents, sessions, prompts, files, and personal agent auth.",
39
+ permissions: [
40
+ "inspect",
41
+ "sessions.read",
42
+ "sessions.write",
43
+ "prompt.send",
44
+ "prompt.abort",
45
+ "files.read",
46
+ "files.write",
47
+ "settings.read",
48
+ "auth.manage",
49
+ "queue.read",
50
+ "queue.write",
51
+ ],
52
+ system: true,
53
+ },
54
+ {
55
+ id: READONLY_GROUP_ID,
56
+ name: "Read Only",
57
+ description: "Read-only access to status, sessions, activity, and artifacts.",
58
+ permissions: [
59
+ "inspect",
60
+ "sessions.read",
61
+ "files.read",
62
+ "settings.read",
63
+ ],
64
+ system: true,
65
+ },
9
66
  ];
10
67
  const COMMAND_PERMISSIONS = new Map([
11
68
  ["start", "inspect"],
@@ -15,141 +72,93 @@ const COMMAND_PERMISSIONS = new Map([
15
72
  ["version", "inspect"],
16
73
  ["channels", "inspect"],
17
74
  ["agents", "inspect"],
18
- ["diagnostics", "admin"],
19
75
  ["tasks", "inspect"],
20
76
  ["progress", "inspect"],
21
- ["activity", "inspect"],
22
- ["audit", "admin"],
23
- ["mirror", "settings"],
24
- ["notify", "settings"],
25
- ["workspaces", "sessions"],
26
- ["voice", "inspect"],
27
- ["agent", "settings"],
28
- ["session", "sessions"],
29
- ["sessions", "sessions"],
30
- ["switch", "sessions"],
31
- ["pinned", "sessions"],
32
- ["pin", "sessions"],
33
- ["unpin", "sessions"],
34
- ["attach", "sessions"],
35
- ["handback", "sessions"],
36
- ["new", "sessions"],
37
- ["sync", "sessions"],
38
- ["lock", "sessions"],
39
- ["unlock", "sessions"],
40
- ["locks", "sessions"],
41
- ["queue", "inspect"],
42
- ["cancel", "prompt"],
43
- ["clearqueue", "prompt"],
44
- ["retry", "prompt"],
45
- ["abort", "prompt"],
46
- ["stop", "prompt"],
47
- ["artifacts", "files"],
48
- ["launch", "settings"],
49
- ["launch_profiles", "settings"],
50
- ["launch-profiles", "settings"],
51
- ["fast", "settings"],
52
- ["model", "settings"],
53
- ["reasoning", "settings"],
54
- ["effort", "settings"],
77
+ ["activity", "sessions.read"],
55
78
  ["auth", "inspect"],
56
- ["login", "auth"],
57
- ["logout", "auth"],
58
- ["logs", "admin"],
59
- ["restart", "admin"],
60
- ["update", "admin"],
79
+ ["voice", "inspect"],
80
+ ["whoami", "inspect"],
81
+ ["diagnostics", "diagnostics.read"],
82
+ ["logs", "logs.read"],
83
+ ["audit", "audit.read"],
84
+ ["restart", "system.restart"],
85
+ ["update", "updates.run"],
86
+ ["workspaces", "sessions.read"],
87
+ ["session", "sessions.read"],
88
+ ["sessions", "sessions.read"],
89
+ ["pinned", "sessions.read"],
90
+ ["locks", "sessions.read"],
91
+ ["queue", "queue.read"],
92
+ ["artifacts", "files.read"],
93
+ ["agent", "settings.write"],
94
+ ["mirror", "settings.write"],
95
+ ["notify", "settings.write"],
96
+ ["launch", "settings.write"],
97
+ ["launch_profiles", "settings.write"],
98
+ ["launch-profiles", "settings.write"],
99
+ ["fast", "settings.write"],
100
+ ["model", "settings.write"],
101
+ ["reasoning", "settings.write"],
102
+ ["effort", "settings.write"],
103
+ ["login", "auth.manage"],
104
+ ["logout", "auth.manage"],
105
+ ["new", "sessions.write"],
106
+ ["switch", "sessions.write"],
107
+ ["attach", "sessions.write"],
108
+ ["handback", "sessions.write"],
109
+ ["sync", "sessions.write"],
110
+ ["pin", "sessions.write"],
111
+ ["unpin", "sessions.write"],
112
+ ["lock", "sessions.write"],
113
+ ["unlock", "sessions.write"],
114
+ ["retry", "prompt.send"],
115
+ ["clearqueue", "queue.write"],
116
+ ["cancel", "queue.write"],
117
+ ["abort", "prompt.abort"],
118
+ ["stop", "prompt.abort"],
119
+ ["register_chat", "users.write"],
120
+ ["chat_access", "users.write"],
121
+ ["link", "inspect"],
61
122
  ]);
62
- export function createDefaultRolePolicies() {
63
- return {
64
- admin: new Set(ALL_PERMISSIONS),
65
- operator: new Set(["inspect", "sessions", "prompt", "files", "settings", "auth"]),
66
- readonly: new Set(["inspect", "sessions"]),
67
- };
68
- }
69
- export function parseRolePoliciesJson(raw) {
70
- const policies = createDefaultRolePolicies();
71
- if (!raw) {
72
- return policies;
73
- }
74
- let parsed;
75
- try {
76
- parsed = JSON.parse(raw);
77
- }
78
- catch (error) {
79
- throw new Error(`Invalid TELEGRAM_ROLE_POLICIES_JSON: ${error instanceof Error ? error.message : String(error)}`);
80
- }
81
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
82
- throw new Error("TELEGRAM_ROLE_POLICIES_JSON must be an object keyed by role");
83
- }
84
- for (const [role, rawPermissions] of Object.entries(parsed)) {
85
- if (!isTelegramRole(role)) {
86
- throw new Error(`Invalid TELEGRAM_ROLE_POLICIES_JSON role: ${role}`);
87
- }
88
- policies[role] = parsePermissionList(rawPermissions, role);
89
- }
90
- if (!policies.admin.has("admin")) {
91
- policies.admin.add("admin");
92
- }
93
- return policies;
94
- }
95
- export function hasTelegramPermission(policies, role, permission) {
96
- return policies[role].has(permission) || policies[role].has("admin");
97
- }
98
123
  export function permissionForCommand(command) {
99
124
  if (!command) {
100
- return "inspect";
125
+ return null;
101
126
  }
102
- return COMMAND_PERMISSIONS.get(command.toLowerCase()) ?? "inspect";
127
+ return COMMAND_PERMISSIONS.get(command.toLowerCase()) ?? null;
103
128
  }
104
129
  export function permissionForCallbackData(callbackData) {
105
130
  if (!callbackData) {
106
- return "inspect";
131
+ return null;
107
132
  }
108
133
  if (callbackData === "noop_page") {
109
134
  return "inspect";
110
135
  }
111
136
  if (/^(sess_|ws_)/.test(callbackData)) {
112
- return "sessions";
137
+ return "sessions.write";
113
138
  }
114
139
  if (/^(launch_|launchconfirm_|model_|effort_|agent_)/.test(callbackData)) {
115
- return "settings";
140
+ return "settings.write";
116
141
  }
117
142
  if (callbackData.startsWith("upd_")) {
118
- return "admin";
143
+ return "updates.run";
119
144
  }
120
145
  if (callbackData.startsWith("approval_") || callbackData.startsWith("codex_abort:") || callbackData.startsWith("agent_abort:")) {
121
- return "prompt";
146
+ return "prompt.abort";
122
147
  }
123
148
  if (callbackData.startsWith("queue_")) {
124
- return "prompt";
149
+ return "queue.write";
150
+ }
151
+ if (callbackData.startsWith("artifact_delete")) {
152
+ return "files.write";
125
153
  }
126
154
  if (callbackData.startsWith("artifact_")) {
127
- return "files";
155
+ return "files.read";
128
156
  }
129
- return "inspect";
157
+ return null;
130
158
  }
131
- export function isTelegramRole(value) {
132
- return value === "admin" || value === "operator" || value === "readonly";
133
- }
134
- function parsePermissionList(rawPermissions, role) {
135
- if (rawPermissions === "*") {
136
- return new Set(ALL_PERMISSIONS);
137
- }
138
- if (!Array.isArray(rawPermissions)) {
139
- throw new Error(`TELEGRAM_ROLE_POLICIES_JSON.${role} must be an array or "*"`);
140
- }
141
- const permissions = new Set();
142
- for (const rawPermission of rawPermissions) {
143
- if (rawPermission === "*") {
144
- return new Set(ALL_PERMISSIONS);
145
- }
146
- if (typeof rawPermission !== "string" || !isTelegramPermission(rawPermission)) {
147
- throw new Error(`Invalid TELEGRAM_ROLE_POLICIES_JSON permission for ${role}: ${String(rawPermission)}`);
148
- }
149
- permissions.add(rawPermission);
150
- }
151
- return permissions;
159
+ export function permissionForWebRequest(method, pathname) {
160
+ return permissionForWebRequestFromContract(method, pathname);
152
161
  }
153
- function isTelegramPermission(value) {
162
+ export function isPermission(value) {
154
163
  return ALL_PERMISSIONS.includes(value);
155
164
  }
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { agentLabel } from "./agent.js";
@@ -34,6 +34,9 @@ export class AgentUpdateManager {
34
34
  }
35
35
  readLog(id) {
36
36
  const job = this.requireJob(id);
37
+ if (job.logDeletedAt) {
38
+ return { job: this.snapshot(job), plain: `Update log was deleted at ${job.logDeletedAt}.` };
39
+ }
37
40
  try {
38
41
  return { job: this.snapshot(job), plain: redactText(readFileSync(job.logPath, "utf8")) };
39
42
  }
@@ -44,6 +47,17 @@ export class AgentUpdateManager {
44
47
  };
45
48
  }
46
49
  }
50
+ deleteLog(id) {
51
+ const job = this.requireJob(id);
52
+ if (job.status === "running") {
53
+ throw new Error("Cannot delete the update log while the update job is still running.");
54
+ }
55
+ const snapshot = this.snapshot(job);
56
+ rmSync(job.logPath, { force: true });
57
+ this.jobs.delete(id);
58
+ this.persistJobs();
59
+ return snapshot;
60
+ }
47
61
  start(agentId, context = {}) {
48
62
  const running = [...this.jobs.values()].find((job) => job.agentId === agentId && job.status === "running");
49
63
  if (running) {
@@ -181,6 +195,10 @@ export class AgentUpdateManager {
181
195
  const parsed = JSON.parse(readFileSync(this.manifestPath, "utf8"));
182
196
  let changed = false;
183
197
  for (const snapshot of parsed) {
198
+ if (snapshot.logDeletedAt) {
199
+ changed = true;
200
+ continue;
201
+ }
184
202
  const staleRunning = snapshot.status === "running" && !isProcessRunning(snapshot.ownerPid);
185
203
  if (staleRunning) {
186
204
  changed = true;