clay-server 4.0.1-beta.1 → 4.1.0-beta.2
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/lib/daemon-project-access.js +118 -0
- package/lib/daemon-projects.js +19 -5
- package/lib/daemon.js +19 -18
- package/lib/project-logs-access-scope.js +94 -0
- package/lib/project-logs-query.js +3 -0
- package/lib/project-logs-service.js +86 -84
- package/lib/project-logs-store.js +5 -3
- package/lib/project-pair-lifecycle.js +140 -102
- package/lib/project-pair-message.js +50 -0
- package/lib/project-pair-replacement-state.js +72 -0
- package/lib/project-pair-task-control.js +264 -0
- package/lib/project-pair-usage.js +76 -0
- package/lib/project-session-pair.js +115 -116
- package/lib/project-sessions.js +25 -1
- package/lib/project-worker-proposal.js +69 -58
- package/lib/project.js +21 -4
- package/lib/public/css/icon-strip.css +40 -0
- package/lib/public/modules/app-messages.js +9 -0
- package/lib/public/modules/project-access.js +141 -0
- package/lib/public/modules/project-grouping.js +22 -0
- package/lib/public/modules/sidebar-projects.js +14 -171
- package/lib/public/modules/worker-proposal.js +4 -0
- package/lib/sdk-bridge.js +20 -0
- package/lib/server-admin.js +3 -3
- package/lib/server-global-ws.js +10 -0
- package/lib/server-mates.js +4 -3
- package/lib/server-palette.js +17 -14
- package/lib/server.js +184 -102
- package/lib/session-pair-factory.js +3 -0
- package/lib/session-pair-history.js +40 -0
- package/lib/session-pair-mcp-server.js +19 -2
- package/lib/session-pair-operation.js +11 -0
- package/lib/session-pair-prompts.js +24 -2
- package/lib/session-pair-turn-control.js +8 -3
- package/lib/session-provenance.js +1 -0
- package/lib/session-spawn-mcp-server.js +2 -1
- package/lib/sessions.js +2 -0
- package/lib/worker-proposal-control.js +69 -0
- package/lib/worker-proposal-decision.js +7 -0
- package/lib/worker-runtime-catalog.js +52 -0
- package/lib/workspace-query-access.js +3 -2
- package/lib/workspace-query-service.js +1 -1
- package/lib/worktree.js +8 -4
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/adapters/codex.js +25 -7
- package/package.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Persisted Clay-level access rules for parent projects and exact worktrees.
|
|
2
|
+
// Filesystem and Git permissions are deliberately outside this module.
|
|
3
|
+
|
|
4
|
+
function parentSlugFor(slug) {
|
|
5
|
+
if (typeof slug !== "string") return null;
|
|
6
|
+
var marker = slug.indexOf("--");
|
|
7
|
+
return marker > 0 ? slug.substring(0, marker) : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findProject(config, slug) {
|
|
11
|
+
var projects = config && Array.isArray(config.projects) ? config.projects : [];
|
|
12
|
+
for (var i = 0; i < projects.length; i++) {
|
|
13
|
+
if (projects[i].slug === slug) return projects[i];
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function uniqueUsers(values) {
|
|
19
|
+
var source = Array.isArray(values) ? values : [];
|
|
20
|
+
var seen = {};
|
|
21
|
+
var out = [];
|
|
22
|
+
for (var i = 0; i < source.length; i++) {
|
|
23
|
+
if (typeof source[i] !== "string" || !source[i] || seen[source[i]]) continue;
|
|
24
|
+
seen[source[i]] = true;
|
|
25
|
+
out.push(source[i]);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getProjectAccess(config, slug, osUsers) {
|
|
31
|
+
var parentSlug = parentSlugFor(slug);
|
|
32
|
+
var project = findProject(config, parentSlug || slug);
|
|
33
|
+
if (!project) return { error: "Project not found" };
|
|
34
|
+
|
|
35
|
+
var isMateProject = project.slug.indexOf("mate-") === 0;
|
|
36
|
+
var visibility = isMateProject ? "private" : (project.visibility || (osUsers ? "private" : "public"));
|
|
37
|
+
var parentAllowedUsers = uniqueUsers(project.allowedUsers);
|
|
38
|
+
if (!parentSlug) {
|
|
39
|
+
return {
|
|
40
|
+
slug: slug,
|
|
41
|
+
visibility: visibility,
|
|
42
|
+
allowedUsers: parentAllowedUsers,
|
|
43
|
+
ownerId: project.ownerId || null,
|
|
44
|
+
isWorktree: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var grants = project.worktreeAllowedUsers && project.worktreeAllowedUsers[slug];
|
|
49
|
+
var worktreeAllowedUsers = uniqueUsers(grants);
|
|
50
|
+
return {
|
|
51
|
+
slug: slug,
|
|
52
|
+
visibility: visibility,
|
|
53
|
+
allowedUsers: uniqueUsers(parentAllowedUsers.concat(worktreeAllowedUsers)),
|
|
54
|
+
ownerId: project.ownerId || null,
|
|
55
|
+
isWorktree: true,
|
|
56
|
+
parentSlug: parentSlug,
|
|
57
|
+
parentAllowedUsers: parentAllowedUsers,
|
|
58
|
+
worktreeAllowedUsers: worktreeAllowedUsers,
|
|
59
|
+
parentAccess: {
|
|
60
|
+
slug: parentSlug,
|
|
61
|
+
visibility: visibility,
|
|
62
|
+
allowedUsers: parentAllowedUsers,
|
|
63
|
+
ownerId: project.ownerId || null,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setAllowedUsers(config, slug, allowedUsers) {
|
|
69
|
+
var parentSlug = parentSlugFor(slug);
|
|
70
|
+
var project = findProject(config, parentSlug || slug);
|
|
71
|
+
if (!project) return { error: "Project not found" };
|
|
72
|
+
var clean = uniqueUsers(allowedUsers);
|
|
73
|
+
if (!parentSlug) {
|
|
74
|
+
project.allowedUsers = clean;
|
|
75
|
+
return { ok: true, isWorktree: false, allowedUsers: clean };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!project.worktreeAllowedUsers || typeof project.worktreeAllowedUsers !== "object" || Array.isArray(project.worktreeAllowedUsers)) {
|
|
79
|
+
project.worktreeAllowedUsers = {};
|
|
80
|
+
}
|
|
81
|
+
if (clean.length > 0) project.worktreeAllowedUsers[slug] = clean;
|
|
82
|
+
else delete project.worktreeAllowedUsers[slug];
|
|
83
|
+
if (Object.keys(project.worktreeAllowedUsers).length === 0) delete project.worktreeAllowedUsers;
|
|
84
|
+
return { ok: true, isWorktree: true, allowedUsers: clean };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearWorktreeGrant(config, slug) {
|
|
88
|
+
var parentSlug = parentSlugFor(slug);
|
|
89
|
+
var project = parentSlug ? findProject(config, parentSlug) : null;
|
|
90
|
+
if (!project || !project.worktreeAllowedUsers || !Object.prototype.hasOwnProperty.call(project.worktreeAllowedUsers, slug)) return false;
|
|
91
|
+
delete project.worktreeAllowedUsers[slug];
|
|
92
|
+
if (Object.keys(project.worktreeAllowedUsers).length === 0) delete project.worktreeAllowedUsers;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function reconcileWorktreeGrants(config, parentSlug, existingSlugs) {
|
|
97
|
+
var project = findProject(config, parentSlug);
|
|
98
|
+
if (!project || !project.worktreeAllowedUsers) return false;
|
|
99
|
+
var existing = {};
|
|
100
|
+
for (var i = 0; i < existingSlugs.length; i++) existing[existingSlugs[i]] = true;
|
|
101
|
+
var changed = false;
|
|
102
|
+
var granted = Object.keys(project.worktreeAllowedUsers);
|
|
103
|
+
for (var j = 0; j < granted.length; j++) {
|
|
104
|
+
if (existing[granted[j]]) continue;
|
|
105
|
+
delete project.worktreeAllowedUsers[granted[j]];
|
|
106
|
+
changed = true;
|
|
107
|
+
}
|
|
108
|
+
if (Object.keys(project.worktreeAllowedUsers).length === 0) delete project.worktreeAllowedUsers;
|
|
109
|
+
return changed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
parentSlugFor: parentSlugFor,
|
|
114
|
+
getProjectAccess: getProjectAccess,
|
|
115
|
+
setAllowedUsers: setAllowedUsers,
|
|
116
|
+
clearWorktreeGrant: clearWorktreeGrant,
|
|
117
|
+
reconcileWorktreeGrants: reconcileWorktreeGrants,
|
|
118
|
+
};
|
package/lib/daemon-projects.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
var fs = require("fs");
|
|
8
|
-
var {
|
|
8
|
+
var { scanWorktreesResult, isWorktree } = require("./worktree");
|
|
9
9
|
var { daemonSync } = require("./daemon-sync");
|
|
10
10
|
var logContext = require("./project-log-context");
|
|
11
|
+
var daemonProjectAccess = require("./daemon-project-access");
|
|
12
|
+
var saveConfig = require("./config").saveConfig;
|
|
11
13
|
|
|
12
14
|
// --- Worktree tracking state ---
|
|
13
15
|
var worktreeRegistry = {}; // parentSlug -> [wtSlug, ...]
|
|
@@ -29,9 +31,13 @@ function isWorktreeSlug(slug) {
|
|
|
29
31
|
* @param {string} parentIcon - icon of parent project
|
|
30
32
|
* @param {string} parentOwnerId - owner user ID
|
|
31
33
|
*/
|
|
32
|
-
function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, projectKnowledgeId, osUserInfo) {
|
|
34
|
+
function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, projectKnowledgeId, osUserInfo, config) {
|
|
33
35
|
if (isWorktree(parentPath, osUserInfo)) return;
|
|
34
|
-
var
|
|
36
|
+
var scan = scanWorktreesResult(parentPath, osUserInfo);
|
|
37
|
+
if (!scan.ok) return;
|
|
38
|
+
var worktrees = scan.worktrees;
|
|
39
|
+
var currentSlugs = worktrees.map(function (worktree) { return parentSlug + "--" + worktree.dirName; });
|
|
40
|
+
if (config && daemonProjectAccess.reconcileWorktreeGrants(config, parentSlug, currentSlugs)) saveConfig(config);
|
|
35
41
|
if (worktrees.length === 0) return;
|
|
36
42
|
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
37
43
|
for (var i = 0; i < worktrees.length; i++) {
|
|
@@ -49,7 +55,7 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
|
|
|
49
55
|
console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)");
|
|
50
56
|
}
|
|
51
57
|
daemonSync.register(getWorktreeSyncKey(parentSlug), function () {
|
|
52
|
-
rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId,
|
|
58
|
+
rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config, projectKnowledgeId, osUserInfo);
|
|
53
59
|
});
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -66,8 +72,15 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
|
|
|
66
72
|
if (worktreeScanning[parentSlug]) return;
|
|
67
73
|
worktreeScanning[parentSlug] = true;
|
|
68
74
|
try {
|
|
69
|
-
var
|
|
75
|
+
var scan = scanWorktreesResult(parentPath, osUserInfo);
|
|
76
|
+
if (!scan.ok) return;
|
|
77
|
+
var discovered = scan.worktrees;
|
|
70
78
|
var changed = false;
|
|
79
|
+
var discoveredSlugs = discovered.map(function (worktree) { return parentSlug + "--" + worktree.dirName; });
|
|
80
|
+
if (config && daemonProjectAccess.reconcileWorktreeGrants(config, parentSlug, discoveredSlugs)) {
|
|
81
|
+
saveConfig(config);
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
71
84
|
var existingSlugs = worktreeRegistry[parentSlug] || [];
|
|
72
85
|
var discoveredNames = {};
|
|
73
86
|
for (var i = 0; i < discovered.length; i++) {
|
|
@@ -96,6 +109,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
|
|
|
96
109
|
if (!discoveredNames[dirName]) {
|
|
97
110
|
if (relay.setProjectLogContextState) relay.setProjectLogContextState(sSlug, "archived");
|
|
98
111
|
relay.removeProject(sSlug);
|
|
112
|
+
if (config && daemonProjectAccess.clearWorktreeGrant(config, sSlug)) saveConfig(config);
|
|
99
113
|
existingSlugs.splice(si, 1);
|
|
100
114
|
console.log("[daemon] Rescan: removed stale worktree:", sSlug);
|
|
101
115
|
changed = true;
|
package/lib/daemon.js
CHANGED
|
@@ -36,6 +36,7 @@ var { createWorktree, removeWorktree, isWorktree } = require("./worktree");
|
|
|
36
36
|
var mates = require("./mates");
|
|
37
37
|
var mateKnowledgeMigration = require("./mate-knowledge-migration");
|
|
38
38
|
var { isWorktreeSlug, scanAndRegisterWorktrees, rescanWorktrees, cleanupWorktreesForParent, getFilteredRemovedProjects, registerWorktreeSlug, unregisterWorktreeSlug } = require("./daemon-projects");
|
|
39
|
+
var daemonProjectAccess = require("./daemon-project-access");
|
|
39
40
|
var projectLogContext = require("./project-log-context");
|
|
40
41
|
|
|
41
42
|
var daemonVersion = require("../package.json").version;
|
|
@@ -186,7 +187,7 @@ function worktreeOsUserInfo(project, requestUser) {
|
|
|
186
187
|
function registerProjectWorktrees(project, requestUser) {
|
|
187
188
|
try {
|
|
188
189
|
var osUserInfo = worktreeOsUserInfo(project, requestUser);
|
|
189
|
-
scanAndRegisterWorktrees(relay, project.path, project.slug, project.icon, project.ownerId, project.projectKnowledgeId, osUserInfo);
|
|
190
|
+
scanAndRegisterWorktrees(relay, project.path, project.slug, project.icon, project.ownerId, project.projectKnowledgeId, osUserInfo, config);
|
|
190
191
|
} catch (e) {
|
|
191
192
|
console.error("[daemon] Failed to scan worktrees for " + project.slug + ": " + e.message);
|
|
192
193
|
}
|
|
@@ -440,6 +441,7 @@ var relay = createServer({
|
|
|
440
441
|
}
|
|
441
442
|
relay.removeProject(slug);
|
|
442
443
|
unregisterWorktreeSlug(wtParent, slug);
|
|
444
|
+
if (daemonProjectAccess.clearWorktreeGrant(config, slug)) saveConfig(config);
|
|
443
445
|
console.log("[daemon] Removed worktree (web):", slug);
|
|
444
446
|
relay.broadcastAll({
|
|
445
447
|
type: "projects_updated",
|
|
@@ -599,6 +601,7 @@ var relay = createServer({
|
|
|
599
601
|
projects: relay.getProjects(),
|
|
600
602
|
projectCount: config.projects.length,
|
|
601
603
|
});
|
|
604
|
+
if (relay.refreshProjectAccess) relay.refreshProjectAccess();
|
|
602
605
|
return { ok: true };
|
|
603
606
|
},
|
|
604
607
|
onGetProjectEnv: function (slug) {
|
|
@@ -960,20 +963,27 @@ var relay = createServer({
|
|
|
960
963
|
}
|
|
961
964
|
}
|
|
962
965
|
}
|
|
966
|
+
if (relay.refreshProjectAccess) relay.refreshProjectAccess();
|
|
963
967
|
return { ok: true };
|
|
964
968
|
}
|
|
965
969
|
}
|
|
966
970
|
return { error: "Project not found" };
|
|
967
971
|
},
|
|
968
972
|
onSetProjectAllowedUsers: function (slug, allowedUsers) {
|
|
973
|
+
var accessBefore = daemonProjectAccess.getProjectAccess(config, slug, config.osUsers);
|
|
974
|
+
var result = daemonProjectAccess.setAllowedUsers(config, slug, allowedUsers);
|
|
975
|
+
if (result.error) return result;
|
|
976
|
+
var parentSlug = daemonProjectAccess.parentSlugFor(slug);
|
|
969
977
|
for (var i = 0; i < config.projects.length; i++) {
|
|
970
|
-
if (config.projects[i].slug === slug) {
|
|
971
|
-
var prev =
|
|
972
|
-
|
|
978
|
+
if (config.projects[i].slug === (parentSlug || slug)) {
|
|
979
|
+
var prev = accessBefore && !accessBefore.error
|
|
980
|
+
? (parentSlug ? accessBefore.worktreeAllowedUsers : accessBefore.allowedUsers)
|
|
981
|
+
: [];
|
|
973
982
|
saveConfig(config);
|
|
974
|
-
console.log("[daemon] Set project allowed users:", slug, "→", allowedUsers.length, "users");
|
|
983
|
+
console.log("[daemon] Set project allowed users:", slug, "→", result.allowedUsers.length, "users");
|
|
975
984
|
// OS users mode: sync ACLs for added/removed users
|
|
976
|
-
|
|
985
|
+
// Exact-worktree grants are Clay-only and never change filesystem ACLs.
|
|
986
|
+
if (config.osUsers && !parentSlug) {
|
|
977
987
|
var projectPath = config.projects[i].path;
|
|
978
988
|
// Grant access to newly added users
|
|
979
989
|
for (var a = 0; a < allowedUsers.length; a++) {
|
|
@@ -994,24 +1004,14 @@ var relay = createServer({
|
|
|
994
1004
|
}
|
|
995
1005
|
}
|
|
996
1006
|
}
|
|
1007
|
+
if (relay.refreshProjectAccess) relay.refreshProjectAccess();
|
|
997
1008
|
return { ok: true };
|
|
998
1009
|
}
|
|
999
1010
|
}
|
|
1000
1011
|
return { error: "Project not found" };
|
|
1001
1012
|
},
|
|
1002
1013
|
onGetProjectAccess: function (slug) {
|
|
1003
|
-
|
|
1004
|
-
if (config.projects[i].slug === slug) {
|
|
1005
|
-
var isMateProject = slug.indexOf("mate-") === 0;
|
|
1006
|
-
return {
|
|
1007
|
-
slug: slug,
|
|
1008
|
-
visibility: isMateProject ? "private" : (config.projects[i].visibility || (config.osUsers ? "private" : "public")),
|
|
1009
|
-
allowedUsers: config.projects[i].allowedUsers || [],
|
|
1010
|
-
ownerId: config.projects[i].ownerId || null,
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
return { error: "Project not found" };
|
|
1014
|
+
return daemonProjectAccess.getProjectAccess(config, slug, config.osUsers);
|
|
1015
1015
|
},
|
|
1016
1016
|
onUserProvisioned: function (userId, linuxUser) {
|
|
1017
1017
|
// Grant ACL on all public projects to the newly provisioned user
|
|
@@ -1048,6 +1048,7 @@ var relay = createServer({
|
|
|
1048
1048
|
if (!result.ok) return result;
|
|
1049
1049
|
// Register the new worktree as ephemeral project
|
|
1050
1050
|
var wtSlug = parentSlug + "--" + dirName;
|
|
1051
|
+
if (daemonProjectAccess.clearWorktreeGrant(config, wtSlug)) saveConfig(config);
|
|
1051
1052
|
var context = projectLogContext.resolveWorktreeContext(result.path, parent.projectKnowledgeId, {
|
|
1052
1053
|
branch: branchName,
|
|
1053
1054
|
baseCommit: baseCommit,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Authorization and record scoping for a Project Logs binding. Worktree-only
|
|
2
|
+
// principals share storage with the parent but never inherit its ledger view.
|
|
3
|
+
|
|
4
|
+
function attachProjectLogsAccessScope(ctx) {
|
|
5
|
+
var projectFor = ctx.projectFor;
|
|
6
|
+
var isMultiUser = ctx.isMultiUser;
|
|
7
|
+
var canAccessProject = ctx.canAccessProject;
|
|
8
|
+
var hasFullProjectAccess = ctx.hasFullProjectAccess;
|
|
9
|
+
var openStore = ctx.openStore;
|
|
10
|
+
|
|
11
|
+
function effectiveStatus(status) {
|
|
12
|
+
if (!status || status.isWorktree !== true || !status.parentSlug) return status;
|
|
13
|
+
var parent = projectFor(status.parentSlug);
|
|
14
|
+
if (!parent) return status;
|
|
15
|
+
return parent.getStatus() || status;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ownsProject(principal, status) {
|
|
19
|
+
if (!status) return false;
|
|
20
|
+
if (isMultiUser()) return !!principal.userId && status.projectOwnerId === principal.userId;
|
|
21
|
+
return !status.projectOwnerId || (!!principal.singleUserOwnerId && status.projectOwnerId === principal.singleUserOwnerId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function projectAuthorization(principal, status) {
|
|
25
|
+
var denied = { allowed: false, worktreeOnly: false };
|
|
26
|
+
if (!status || status.isMate === true) return denied;
|
|
27
|
+
var governing = effectiveStatus(status);
|
|
28
|
+
if (ownsProject(principal, governing)) return { allowed: true, worktreeOnly: false };
|
|
29
|
+
if (!isMultiUser() || !principal.userId || typeof canAccessProject !== "function") return denied;
|
|
30
|
+
if (canAccessProject(principal.userId, status) !== true) return denied;
|
|
31
|
+
var full = status.isWorktree !== true;
|
|
32
|
+
if (!full && typeof hasFullProjectAccess === "function") {
|
|
33
|
+
full = hasFullProjectAccess(principal.userId, status) === true;
|
|
34
|
+
}
|
|
35
|
+
return { allowed: true, worktreeOnly: !full };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function authorizedProject(principal, status) {
|
|
39
|
+
return projectAuthorization(principal, status).allowed === true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function storeForStatus(status) {
|
|
43
|
+
var governing = effectiveStatus(status);
|
|
44
|
+
if (!governing || !governing.path) throw new Error("Project Logs are unavailable for this project.");
|
|
45
|
+
return openStore(governing.path, { projectKnowledgeId: governing.projectKnowledgeId || null });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function recordContext(status) {
|
|
49
|
+
if (!status || status.isWorktree !== true || !status.changeSetId) return null;
|
|
50
|
+
return {
|
|
51
|
+
kind: "worktree",
|
|
52
|
+
changeSetId: status.changeSetId,
|
|
53
|
+
branch: status.branch || null,
|
|
54
|
+
baseCommit: status.baseCommit || null,
|
|
55
|
+
headCommit: status.headCommit || null,
|
|
56
|
+
status: "active",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function scopedArgs(args, status, worktreeOnly) {
|
|
61
|
+
var out = Object.assign({}, args || {});
|
|
62
|
+
if (worktreeOnly) {
|
|
63
|
+
out.contextMode = "current";
|
|
64
|
+
out.exactChangeSet = true;
|
|
65
|
+
} else if (!out.contextMode) {
|
|
66
|
+
out.contextMode = status && status.isWorktree ? "current" : "project";
|
|
67
|
+
}
|
|
68
|
+
out.currentChangeSetId = status && status.changeSetId || null;
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function visibleEntry(store, ref, status, worktreeOnly) {
|
|
73
|
+
var entry = store.read(ref, false);
|
|
74
|
+
if (!entry) throw new Error("Log entry not found.");
|
|
75
|
+
if (worktreeOnly) {
|
|
76
|
+
var changeSetId = entry.context && entry.context.changeSetId;
|
|
77
|
+
if (!changeSetId || changeSetId !== status.changeSetId) throw new Error("Log entry not found.");
|
|
78
|
+
}
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
effectiveStatus: effectiveStatus,
|
|
84
|
+
ownsProject: ownsProject,
|
|
85
|
+
projectAuthorization: projectAuthorization,
|
|
86
|
+
authorizedProject: authorizedProject,
|
|
87
|
+
storeForStatus: storeForStatus,
|
|
88
|
+
recordContext: recordContext,
|
|
89
|
+
scopedArgs: scopedArgs,
|
|
90
|
+
visibleEntry: visibleEntry,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { attachProjectLogsAccessScope: attachProjectLogsAccessScope };
|
|
@@ -70,6 +70,9 @@ function contextMode(options) {
|
|
|
70
70
|
function matchesContext(entry, options) {
|
|
71
71
|
var mode = contextMode(options);
|
|
72
72
|
var changeSetId = entry && entry.context && entry.context.changeSetId;
|
|
73
|
+
if (options && options.exactChangeSet === true) {
|
|
74
|
+
return !!changeSetId && changeSetId === options.currentChangeSetId;
|
|
75
|
+
}
|
|
73
76
|
if (mode === "all") return true;
|
|
74
77
|
if (mode === "project") return !changeSetId;
|
|
75
78
|
return !changeSetId || changeSetId === options.currentChangeSetId;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
var builtinMates = require("./builtin-mates");
|
|
23
23
|
var logsStore = require("./project-logs-store");
|
|
24
|
+
var attachProjectLogsAccessScope = require("./project-logs-access-scope").attachProjectLogsAccessScope;
|
|
24
25
|
var LOG_REF_PATTERN = /^log:[A-Za-z0-9_-]{24}$/;
|
|
25
26
|
|
|
26
27
|
function readOnly() {
|
|
@@ -52,7 +53,6 @@ function attachProjectLogsService(ctx) {
|
|
|
52
53
|
var getProjects = context.getProjects;
|
|
53
54
|
var isMultiUser = context.isMultiUser;
|
|
54
55
|
var resolveMate = context.resolveMate;
|
|
55
|
-
var canAccessProject = context.canAccessProject;
|
|
56
56
|
var findUserById = context.findUserById || function () { return null; };
|
|
57
57
|
var storeCache = new Map();
|
|
58
58
|
|
|
@@ -75,61 +75,21 @@ function attachProjectLogsService(ctx) {
|
|
|
75
75
|
return (projects && projects.get(slug)) || null;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
function authorizedProject(principal, status) {
|
|
95
|
-
if (!status) return false;
|
|
96
|
-
if (status.isMate === true) return false;
|
|
97
|
-
var governing = effectiveStatus(status);
|
|
98
|
-
if (ownsProject(principal, governing)) return true;
|
|
99
|
-
if (isMultiUser() && principal.userId && typeof canAccessProject === "function") {
|
|
100
|
-
return canAccessProject(principal.userId, governing) === true;
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Open the store for the governing project. When a worktree's parent is
|
|
106
|
-
// registered, its path is authoritative, so worktree Logs are shared without
|
|
107
|
-
// depending on a Git subprocess. The store's own root resolution remains the
|
|
108
|
-
// fallback for unregistered parents and for subdirectories of a checkout.
|
|
109
|
-
function storeForStatus(status) {
|
|
110
|
-
var governing = effectiveStatus(status);
|
|
111
|
-
if (!governing || !governing.path) throw new Error("Project Logs are unavailable for this project.");
|
|
112
|
-
return openStore(governing.path, { projectKnowledgeId: governing.projectKnowledgeId || null });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function recordContext(status) {
|
|
116
|
-
if (!status || status.isWorktree !== true || !status.changeSetId) return null;
|
|
117
|
-
return {
|
|
118
|
-
kind: "worktree",
|
|
119
|
-
changeSetId: status.changeSetId,
|
|
120
|
-
branch: status.branch || null,
|
|
121
|
-
baseCommit: status.baseCommit || null,
|
|
122
|
-
headCommit: status.headCommit || null,
|
|
123
|
-
status: "active",
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function scopedArgs(args, status) {
|
|
128
|
-
var out = Object.assign({}, args || {});
|
|
129
|
-
if (!out.contextMode) out.contextMode = status && status.isWorktree ? "current" : "project";
|
|
130
|
-
out.currentChangeSetId = status && status.changeSetId || null;
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
78
|
+
var logScope = attachProjectLogsAccessScope({
|
|
79
|
+
projectFor: projectFor,
|
|
80
|
+
isMultiUser: isMultiUser,
|
|
81
|
+
canAccessProject: context.canAccessProject,
|
|
82
|
+
hasFullProjectAccess: context.hasFullProjectAccess,
|
|
83
|
+
openStore: openStore,
|
|
84
|
+
});
|
|
85
|
+
var effectiveStatus = logScope.effectiveStatus;
|
|
86
|
+
var ownsProject = logScope.ownsProject;
|
|
87
|
+
var projectAuthorization = logScope.projectAuthorization;
|
|
88
|
+
var authorizedProject = logScope.authorizedProject;
|
|
89
|
+
var storeForStatus = logScope.storeForStatus;
|
|
90
|
+
var recordContext = logScope.recordContext;
|
|
91
|
+
var scopedArgs = logScope.scopedArgs;
|
|
92
|
+
var visibleEntry = logScope.visibleEntry;
|
|
133
93
|
|
|
134
94
|
function statusFor(principal, slug) {
|
|
135
95
|
var project = projectFor(slug);
|
|
@@ -317,6 +277,20 @@ function attachProjectLogsService(ctx) {
|
|
|
317
277
|
return project.getStatus();
|
|
318
278
|
}
|
|
319
279
|
|
|
280
|
+
function currentScope() {
|
|
281
|
+
var current = revalidate();
|
|
282
|
+
var project = projectFor(current.principal.projectSlug);
|
|
283
|
+
var status = project && project.getStatus();
|
|
284
|
+
var decision = projectAuthorization(current.principal, status);
|
|
285
|
+
if (!decision.allowed) throw new Error("Project Logs are not available for this project.");
|
|
286
|
+
return { status: status, worktreeOnly: decision.worktreeOnly };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function scopedEntry(args) {
|
|
290
|
+
var scope = currentScope();
|
|
291
|
+
return visibleEntry(live(), args && args.ref, scope.status, scope.worktreeOnly);
|
|
292
|
+
}
|
|
293
|
+
|
|
320
294
|
function mutationContext() {
|
|
321
295
|
return recordContext(currentStatus());
|
|
322
296
|
}
|
|
@@ -336,8 +310,9 @@ function attachProjectLogsService(ctx) {
|
|
|
336
310
|
root: store.root,
|
|
337
311
|
author: initial.author,
|
|
338
312
|
listLogs: function (args) {
|
|
339
|
-
var
|
|
340
|
-
var
|
|
313
|
+
var scope = currentScope();
|
|
314
|
+
var status = scope.status;
|
|
315
|
+
var options = scopedArgs(args, status, scope.worktreeOnly);
|
|
341
316
|
var result = live().list(options);
|
|
342
317
|
result.contextMode = options.contextMode;
|
|
343
318
|
result.currentChangeSetId = status.changeSetId || null;
|
|
@@ -345,8 +320,9 @@ function attachProjectLogsService(ctx) {
|
|
|
345
320
|
return result;
|
|
346
321
|
},
|
|
347
322
|
searchLogs: function (args) {
|
|
348
|
-
var
|
|
349
|
-
var
|
|
323
|
+
var scope = currentScope();
|
|
324
|
+
var status = scope.status;
|
|
325
|
+
var options = scopedArgs(args, status, scope.worktreeOnly);
|
|
350
326
|
var result = live().search(options);
|
|
351
327
|
result.contextMode = options.contextMode;
|
|
352
328
|
result.currentChangeSetId = status.changeSetId || null;
|
|
@@ -354,21 +330,22 @@ function attachProjectLogsService(ctx) {
|
|
|
354
330
|
return result;
|
|
355
331
|
},
|
|
356
332
|
readLog: function (args) {
|
|
357
|
-
|
|
358
|
-
if (!entry) throw new Error("Log entry not found.");
|
|
359
|
-
return entry;
|
|
333
|
+
return scopedEntry(args);
|
|
360
334
|
},
|
|
361
|
-
logHistory: function (args) { return live().history(args && args.ref, args || {}); },
|
|
362
|
-
commentLog: function (args) { return live().comment(args && args.ref, args || {}, author(), mutationContext()); },
|
|
363
|
-
readLogRevision: function (args) { return live().readRevision(args && args.ref, args && args.revision); },
|
|
364
|
-
listLogFeedback: canonical ? function (args) {
|
|
365
|
-
|
|
335
|
+
logHistory: function (args) { scopedEntry(args); return live().history(args && args.ref, args || {}); },
|
|
336
|
+
commentLog: function (args) { scopedEntry(args); return live().comment(args && args.ref, args || {}, author(), mutationContext()); },
|
|
337
|
+
readLogRevision: function (args) { scopedEntry(args); return live().readRevision(args && args.ref, args && args.revision); },
|
|
338
|
+
listLogFeedback: canonical ? function (args) {
|
|
339
|
+
var scope = currentScope();
|
|
340
|
+
return live().feedback(scopedArgs(args, scope.status, scope.worktreeOnly));
|
|
341
|
+
} : driverOnly,
|
|
342
|
+
reviewLogComment: canonical ? function (args) { scopedEntry(args); return live().review(args && args.ref, args || {}, author(), mutationContext()); } : driverOnly,
|
|
366
343
|
revertLog: canonical
|
|
367
|
-
? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author(), mutationContext()); }
|
|
344
|
+
? function (args) { scopedEntry(args); return live().revert(args && args.ref, args && args.revision, args && args.reason, author(), mutationContext()); }
|
|
368
345
|
: driverOnly,
|
|
369
346
|
createLog: canonical ? function (args) { return live().create(args || {}, author(), mutationContext()); } : agentOnly,
|
|
370
|
-
updateLog: canonical ? function (args) { return live().update(args && args.ref, args || {}, author(), mutationContext()); } : agentOnly,
|
|
371
|
-
linkLog: canonical ? function (args) { return live().link(args && args.ref, args && args.links, author(), mutationContext()); } : agentOnly,
|
|
347
|
+
updateLog: canonical ? function (args) { scopedEntry(args); return live().update(args && args.ref, args || {}, author(), mutationContext()); } : agentOnly,
|
|
348
|
+
linkLog: canonical ? function (args) { scopedEntry(args); return live().link(args && args.ref, args && args.links, author(), mutationContext()); } : agentOnly,
|
|
372
349
|
removeLog: canDelete ? function (args) {
|
|
373
350
|
var current = revalidate();
|
|
374
351
|
var project = projectFor(current.principal.projectSlug);
|
|
@@ -376,6 +353,7 @@ function attachProjectLogsService(ctx) {
|
|
|
376
353
|
if (!project || !authorizedProject(current.principal, currentStatus)) {
|
|
377
354
|
throw new Error("Project Logs are not available for this project.");
|
|
378
355
|
}
|
|
356
|
+
visibleEntry(storeForStatus(currentStatus), args && args.ref, currentStatus, projectAuthorization(current.principal, currentStatus).worktreeOnly);
|
|
379
357
|
if (current.principal.kind !== "session" && !ownsProject(current.principal, currentStatus)) ownerOnly();
|
|
380
358
|
return storeForStatus(currentStatus).remove(args && args.ref, current.author, recordContext(currentStatus));
|
|
381
359
|
} : ownerOnly,
|
|
@@ -394,7 +372,17 @@ function attachProjectLogsService(ctx) {
|
|
|
394
372
|
var principal = revalidate();
|
|
395
373
|
var slug = args && args.projectSlug;
|
|
396
374
|
if (!slug) throw new Error("An exact project slug is required.");
|
|
397
|
-
|
|
375
|
+
var status = statusFor(principal, slug);
|
|
376
|
+
var decision = projectAuthorization(principal, status);
|
|
377
|
+
return { store: storeForStatus(status), status: status, worktreeOnly: decision.worktreeOnly };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function targetEntry(args) {
|
|
381
|
+
var selected = target(args);
|
|
382
|
+
return {
|
|
383
|
+
selected: selected,
|
|
384
|
+
entry: visibleEntry(selected.store, args && args.ref, selected.status, selected.worktreeOnly),
|
|
385
|
+
};
|
|
398
386
|
}
|
|
399
387
|
|
|
400
388
|
function resolveLogNavigation(args) {
|
|
@@ -408,15 +396,19 @@ function attachProjectLogsService(ctx) {
|
|
|
408
396
|
projects.forEach(function (project, slug) {
|
|
409
397
|
if (!project || typeof project.getStatus !== "function") return;
|
|
410
398
|
var status = project.getStatus();
|
|
411
|
-
|
|
399
|
+
var decision = projectAuthorization(principal, status);
|
|
400
|
+
if (!decision.allowed) return;
|
|
412
401
|
var governing = effectiveStatus(status);
|
|
413
402
|
var scope = governing && (governing.projectKnowledgeId || governing.path);
|
|
414
|
-
|
|
415
|
-
seenScopes.
|
|
403
|
+
var scopeKey = decision.worktreeOnly ? scope + ":" + (status.changeSetId || slug) : scope;
|
|
404
|
+
if (!scope || seenScopes.has(scopeKey)) return;
|
|
405
|
+
seenScopes.add(scopeKey);
|
|
416
406
|
var entry = null;
|
|
417
|
-
try { entry = storeForStatus(status)
|
|
407
|
+
try { entry = visibleEntry(storeForStatus(status), ref, status, decision.worktreeOnly); } catch (e) { return; }
|
|
418
408
|
if (!entry) return;
|
|
419
|
-
var targetSlug =
|
|
409
|
+
var targetSlug = decision.worktreeOnly
|
|
410
|
+
? slug
|
|
411
|
+
: (status.isWorktree && status.parentSlug && projectFor(status.parentSlug) ? status.parentSlug : slug);
|
|
420
412
|
if (match && match.projectSlug !== targetSlug) throw new Error("Log reference is ambiguous.");
|
|
421
413
|
match = { projectSlug: targetSlug, ref: ref };
|
|
422
414
|
});
|
|
@@ -431,16 +423,26 @@ function attachProjectLogsService(ctx) {
|
|
|
431
423
|
canDelete: false,
|
|
432
424
|
projectSlug: null,
|
|
433
425
|
author: null,
|
|
434
|
-
listLogs: function (args) {
|
|
435
|
-
|
|
426
|
+
listLogs: function (args) {
|
|
427
|
+
var selected = target(args);
|
|
428
|
+
return selected.store.list(scopedArgs(args, selected.status, selected.worktreeOnly));
|
|
429
|
+
},
|
|
430
|
+
searchLogs: function (args) {
|
|
431
|
+
var selected = target(args);
|
|
432
|
+
return selected.store.search(scopedArgs(args, selected.status, selected.worktreeOnly));
|
|
433
|
+
},
|
|
436
434
|
readLog: function (args) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
435
|
+
return targetEntry(args).entry;
|
|
436
|
+
},
|
|
437
|
+
logHistory: function (args) {
|
|
438
|
+
var selected = targetEntry(args).selected;
|
|
439
|
+
return selected.store.history(args && args.ref, args || {});
|
|
440
440
|
},
|
|
441
|
-
logHistory: function (args) { return target(args).history(args && args.ref, args || {}); },
|
|
442
441
|
commentLog: readOnly,
|
|
443
|
-
readLogRevision: function (args) {
|
|
442
|
+
readLogRevision: function (args) {
|
|
443
|
+
var selected = targetEntry(args).selected;
|
|
444
|
+
return selected.store.readRevision(args && args.ref, args && args.revision);
|
|
445
|
+
},
|
|
444
446
|
resolveLogNavigation: resolveLogNavigation,
|
|
445
447
|
listLogFeedback: readOnly,
|
|
446
448
|
reviewLogComment: readOnly,
|