clay-server 4.0.1-beta.1 → 4.1.0-beta.1

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.
Files changed (43) hide show
  1. package/lib/daemon-project-access.js +118 -0
  2. package/lib/daemon-projects.js +19 -5
  3. package/lib/daemon.js +19 -18
  4. package/lib/project-logs-access-scope.js +94 -0
  5. package/lib/project-logs-query.js +3 -0
  6. package/lib/project-logs-service.js +86 -84
  7. package/lib/project-logs-store.js +5 -3
  8. package/lib/project-pair-lifecycle.js +71 -61
  9. package/lib/project-pair-message.js +50 -0
  10. package/lib/project-pair-replacement-state.js +72 -0
  11. package/lib/project-pair-task-control.js +264 -0
  12. package/lib/project-pair-usage.js +73 -0
  13. package/lib/project-session-pair.js +115 -116
  14. package/lib/project-sessions.js +25 -1
  15. package/lib/project-worker-proposal.js +54 -57
  16. package/lib/project.js +21 -4
  17. package/lib/public/css/icon-strip.css +40 -0
  18. package/lib/public/modules/app-messages.js +9 -0
  19. package/lib/public/modules/project-access.js +141 -0
  20. package/lib/public/modules/project-grouping.js +22 -0
  21. package/lib/public/modules/sidebar-projects.js +14 -171
  22. package/lib/public/modules/worker-proposal.js +4 -0
  23. package/lib/sdk-bridge.js +20 -0
  24. package/lib/server-admin.js +3 -3
  25. package/lib/server-global-ws.js +10 -0
  26. package/lib/server-mates.js +4 -3
  27. package/lib/server-palette.js +17 -14
  28. package/lib/server.js +184 -102
  29. package/lib/session-pair-factory.js +3 -0
  30. package/lib/session-pair-history.js +40 -0
  31. package/lib/session-pair-mcp-server.js +12 -1
  32. package/lib/session-pair-operation.js +11 -0
  33. package/lib/session-pair-prompts.js +24 -2
  34. package/lib/session-pair-turn-control.js +8 -3
  35. package/lib/worker-proposal-control.js +69 -0
  36. package/lib/worker-proposal-decision.js +7 -0
  37. package/lib/worker-runtime-catalog.js +52 -0
  38. package/lib/workspace-query-access.js +3 -2
  39. package/lib/workspace-query-service.js +1 -1
  40. package/lib/worktree.js +8 -4
  41. package/lib/ws-schema.js +1 -0
  42. package/lib/yoke/adapters/codex.js +6 -2
  43. 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
+ };
@@ -5,9 +5,11 @@
5
5
  */
6
6
 
7
7
  var fs = require("fs");
8
- var { scanWorktrees, isWorktree } = require("./worktree");
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 worktrees = scanWorktrees(parentPath, osUserInfo);
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, null, projectKnowledgeId, osUserInfo);
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 discovered = scanWorktrees(parentPath, osUserInfo);
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 = config.projects[i].allowedUsers || [];
972
- config.projects[i].allowedUsers = allowedUsers;
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
- if (config.osUsers) {
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
- for (var i = 0; i < config.projects.length; i++) {
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
- // A worktree inherits its parent's visibility, so authorization is decided
79
- // against the parent whenever the parent is registered.
80
- function effectiveStatus(status) {
81
- if (!status || status.isWorktree !== true || !status.parentSlug) return status;
82
- var parent = projectFor(status.parentSlug);
83
- if (!parent) return status;
84
- var parentStatus = parent.getStatus();
85
- return parentStatus || status;
86
- }
87
-
88
- function ownsProject(principal, status) {
89
- if (!status) return false;
90
- if (isMultiUser()) return !!principal.userId && status.projectOwnerId === principal.userId;
91
- return !status.projectOwnerId || (!!principal.singleUserOwnerId && status.projectOwnerId === principal.singleUserOwnerId);
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 status = currentStatus();
340
- var options = scopedArgs(args, status);
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 status = currentStatus();
349
- var options = scopedArgs(args, status);
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
- var entry = live().read(args && args.ref, false);
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) { return live().feedback(args || {}); } : driverOnly,
365
- reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author(), mutationContext()); } : driverOnly,
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
- return storeForStatus(statusFor(principal, slug));
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
- if (!authorizedProject(principal, status)) return;
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
- if (!scope || seenScopes.has(scope)) return;
415
- seenScopes.add(scope);
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).read(ref, false); } catch (e) { return; }
407
+ try { entry = visibleEntry(storeForStatus(status), ref, status, decision.worktreeOnly); } catch (e) { return; }
418
408
  if (!entry) return;
419
- var targetSlug = status.isWorktree && status.parentSlug && projectFor(status.parentSlug) ? status.parentSlug : slug;
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) { return target(args).list(args || {}); },
435
- searchLogs: function (args) { return target(args).search(args || {}); },
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
- var entry = target(args).read(args && args.ref, false);
438
- if (!entry) throw new Error("Log entry not found.");
439
- return entry;
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) { return target(args).readRevision(args && args.ref, args && args.revision); },
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,