tmux-team 2.2.0 → 3.0.0-alpha.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.
@@ -1,293 +0,0 @@
1
- // ─────────────────────────────────────────────────────────────
2
- // Permission system for PM commands
3
- // ─────────────────────────────────────────────────────────────
4
-
5
- import { execSync } from 'child_process';
6
- import type { ResolvedConfig, PaneEntry } from '../types.js';
7
-
8
- /**
9
- * Permission expression format:
10
- * pm:<resource>:<action>(<field1>,<field2>,...)
11
- *
12
- * Examples:
13
- * - pm:task:list
14
- * - pm:task:update(status)
15
- * - pm:task:update(assignee,status)
16
- * - pm:task:update(*) - wildcard, any field
17
- * - pm:task:update - no fields, entire action
18
- */
19
-
20
- export interface PermissionCheck {
21
- resource: string; // task, milestone, team, doc, log
22
- action: string; // list, show, create, update, read
23
- fields: string[]; // sorted alphabetically
24
- }
25
-
26
- /**
27
- * Build permission path from command context.
28
- */
29
- export function buildPermissionPath(check: PermissionCheck): string {
30
- const base = `pm:${check.resource}:${check.action}`;
31
- if (check.fields.length === 0) {
32
- return base;
33
- }
34
- // Sort fields alphabetically for canonical form
35
- const sortedFields = [...check.fields].sort();
36
- return `${base}(${sortedFields.join(',')})`;
37
- }
38
-
39
- /**
40
- * Parse a deny pattern into its components.
41
- */
42
- function parsePattern(pattern: string): {
43
- resource: string;
44
- action: string;
45
- fields: string[] | '*' | null;
46
- } {
47
- // Pattern format: pm:resource:action or pm:resource:action(fields) or pm:resource:action(*)
48
- const match = pattern.match(/^pm:(\w+):(\w+)(?:\(([^)]*)\))?$/);
49
- if (!match) {
50
- return { resource: '', action: '', fields: null };
51
- }
52
-
53
- const [, resource, action, fieldsStr] = match;
54
-
55
- if (fieldsStr === undefined) {
56
- // No parentheses - blocks entire action
57
- return { resource, action, fields: null };
58
- }
59
-
60
- if (fieldsStr === '*') {
61
- // Wildcard - blocks any field
62
- return { resource, action, fields: '*' };
63
- }
64
-
65
- // Specific fields
66
- const fields = fieldsStr
67
- .split(',')
68
- .map((f) => f.trim())
69
- .filter(Boolean);
70
- return { resource, action, fields };
71
- }
72
-
73
- /**
74
- * Check if a permission path matches a deny pattern.
75
- */
76
- function matchesPattern(path: PermissionCheck, pattern: string): boolean {
77
- const parsed = parsePattern(pattern);
78
-
79
- // Resource and action must match
80
- if (parsed.resource !== path.resource || parsed.action !== path.action) {
81
- return false;
82
- }
83
-
84
- // No fields in pattern = block entire action
85
- if (parsed.fields === null) {
86
- return true;
87
- }
88
-
89
- // Wildcard = block if path has any fields
90
- if (parsed.fields === '*') {
91
- return path.fields.length > 0;
92
- }
93
-
94
- // Specific fields = block if path uses ANY of the denied fields
95
- return parsed.fields.some((f) => path.fields.includes(f));
96
- }
97
-
98
- /**
99
- * Get current tmux pane in "window.pane" format.
100
- * Returns null if not running in tmux.
101
- */
102
- function getCurrentPane(): string | null {
103
- // Check if we're in tmux
104
- if (!process.env.TMUX) {
105
- return null;
106
- }
107
-
108
- // TMUX_PANE contains the pane ID (e.g., %130) for the shell that's running.
109
- // We must use -t "$TMUX_PANE" to get the correct pane, otherwise tmux returns
110
- // the currently focused pane which may be different when commands are sent
111
- // via send-keys from another pane.
112
- // See: https://github.com/tmux/tmux/issues/4638
113
- const tmuxPane = process.env.TMUX_PANE;
114
- if (!tmuxPane) {
115
- return null;
116
- }
117
-
118
- try {
119
- const result = execSync(
120
- `tmux display-message -p -t "${tmuxPane}" '#{window_index}.#{pane_index}'`,
121
- {
122
- encoding: 'utf-8',
123
- timeout: 1000,
124
- }
125
- ).trim();
126
- return result || null;
127
- } catch {
128
- return null;
129
- }
130
- }
131
-
132
- /**
133
- * Look up agent name by pane ID in the registry.
134
- */
135
- function findAgentByPane(paneRegistry: Record<string, PaneEntry>, paneId: string): string | null {
136
- for (const [agentName, entry] of Object.entries(paneRegistry)) {
137
- if (entry.pane === paneId) {
138
- return agentName;
139
- }
140
- }
141
- return null;
142
- }
143
-
144
- export interface ActorResolution {
145
- actor: string;
146
- source: 'pane' | 'env' | 'default';
147
- warning?: string;
148
- }
149
-
150
- /**
151
- * Resolve current actor using pane registry as primary source.
152
- *
153
- * Priority:
154
- * 1. Look up current tmux pane in pane registry → agent name
155
- * 2. If not in registry → 'human' (full access)
156
- *
157
- * Warnings:
158
- * - If TMT_AGENT_NAME is set but conflicts with pane registry → warn about spoofing
159
- * - If TMT_AGENT_NAME is set but pane not in registry → warn about unregistered pane
160
- */
161
- export function resolveActor(paneRegistry: Record<string, PaneEntry>): ActorResolution {
162
- const envActor = process.env.TMT_AGENT_NAME || process.env.TMUX_TEAM_ACTOR;
163
- const currentPane = getCurrentPane();
164
-
165
- // Not in tmux - use env var or default to human
166
- if (!currentPane) {
167
- if (envActor) {
168
- return { actor: envActor, source: 'env' };
169
- }
170
- return { actor: 'human', source: 'default' };
171
- }
172
-
173
- // In tmux - look up pane in registry
174
- const paneAgent = findAgentByPane(paneRegistry, currentPane);
175
-
176
- if (paneAgent) {
177
- // Pane is registered to an agent
178
- if (envActor && envActor !== paneAgent) {
179
- // Env var conflicts with pane registry - warn about potential spoofing
180
- return {
181
- actor: paneAgent,
182
- source: 'pane',
183
- warning: `⚠️ Identity mismatch: TMT_AGENT_NAME="${envActor}" but pane ${currentPane} is registered to "${paneAgent}". Using pane identity.`,
184
- };
185
- }
186
- return { actor: paneAgent, source: 'pane' };
187
- }
188
-
189
- // Pane not in registry
190
- if (envActor) {
191
- // Agent claims identity but pane not registered - use env identity with warning
192
- // Security: Still apply agent's deny patterns to prevent bypass via unregistered pane
193
- return {
194
- actor: envActor,
195
- source: 'env',
196
- warning: `⚠️ Unregistered pane: pane ${currentPane} is not in registry. Using TMT_AGENT_NAME="${envActor}".`,
197
- };
198
- }
199
-
200
- // Not registered, no env var - human
201
- return { actor: 'human', source: 'default' };
202
- }
203
-
204
- /**
205
- * Get current actor (agent name or 'human').
206
- * Legacy function for backward compatibility.
207
- * Reads from TMT_AGENT_NAME or TMUX_TEAM_ACTOR env vars.
208
- */
209
- export function getCurrentActor(): string {
210
- return process.env.TMT_AGENT_NAME || process.env.TMUX_TEAM_ACTOR || 'human';
211
- }
212
-
213
- export interface PermissionResult {
214
- allowed: boolean;
215
- actor: string;
216
- source: 'pane' | 'env' | 'default';
217
- warning?: string;
218
- }
219
-
220
- /**
221
- * Check if an action is allowed for the current actor.
222
- * Uses pane-based identity resolution with warnings for conflicts.
223
- */
224
- export function checkPermission(config: ResolvedConfig, check: PermissionCheck): PermissionResult {
225
- const resolution = resolveActor(config.paneRegistry);
226
- const { actor, source, warning } = resolution;
227
-
228
- // Human is always allowed (no deny patterns for human)
229
- if (actor === 'human') {
230
- return { allowed: true, actor, source, warning };
231
- }
232
-
233
- // Get agent config
234
- const agentConfig = config.agents[actor];
235
- if (!agentConfig || !agentConfig.deny || agentConfig.deny.length === 0) {
236
- // No deny patterns = allow all
237
- return { allowed: true, actor, source, warning };
238
- }
239
-
240
- // Check if any deny pattern matches
241
- for (const pattern of agentConfig.deny) {
242
- if (matchesPattern(check, pattern)) {
243
- return { allowed: false, actor, source, warning };
244
- }
245
- }
246
-
247
- return { allowed: true, actor, source, warning };
248
- }
249
-
250
- /**
251
- * Simple permission check (legacy, for tests).
252
- * Returns true if allowed, false if denied.
253
- */
254
- export function checkPermissionSimple(config: ResolvedConfig, check: PermissionCheck): boolean {
255
- return checkPermission(config, check).allowed;
256
- }
257
-
258
- /**
259
- * Build permission check for common PM operations.
260
- */
261
- export const PermissionChecks = {
262
- // Task operations
263
- taskList: (): PermissionCheck => ({ resource: 'task', action: 'list', fields: [] }),
264
- taskShow: (): PermissionCheck => ({ resource: 'task', action: 'show', fields: [] }),
265
- taskCreate: (): PermissionCheck => ({ resource: 'task', action: 'create', fields: [] }),
266
- taskUpdate: (fields: string[]): PermissionCheck => ({
267
- resource: 'task',
268
- action: 'update',
269
- fields,
270
- }),
271
- taskDelete: (): PermissionCheck => ({ resource: 'task', action: 'delete', fields: [] }),
272
-
273
- // Milestone operations
274
- milestoneList: (): PermissionCheck => ({ resource: 'milestone', action: 'list', fields: [] }),
275
- milestoneCreate: (): PermissionCheck => ({ resource: 'milestone', action: 'create', fields: [] }),
276
- milestoneUpdate: (fields: string[]): PermissionCheck => ({
277
- resource: 'milestone',
278
- action: 'update',
279
- fields,
280
- }),
281
- milestoneDelete: (): PermissionCheck => ({ resource: 'milestone', action: 'delete', fields: [] }),
282
-
283
- // Doc operations
284
- docRead: (): PermissionCheck => ({ resource: 'doc', action: 'read', fields: [] }),
285
- docUpdate: (): PermissionCheck => ({ resource: 'doc', action: 'update', fields: [] }),
286
-
287
- // Team operations
288
- teamCreate: (): PermissionCheck => ({ resource: 'team', action: 'create', fields: [] }),
289
- teamList: (): PermissionCheck => ({ resource: 'team', action: 'list', fields: [] }),
290
-
291
- // Log operations
292
- logRead: (): PermissionCheck => ({ resource: 'log', action: 'read', fields: [] }),
293
- };
@@ -1,57 +0,0 @@
1
- // ─────────────────────────────────────────────────────────────
2
- // Storage adapter interface for PM backends
3
- // ─────────────────────────────────────────────────────────────
4
-
5
- import type {
6
- Team,
7
- Milestone,
8
- Task,
9
- AuditEvent,
10
- CreateTaskInput,
11
- UpdateTaskInput,
12
- CreateMilestoneInput,
13
- UpdateMilestoneInput,
14
- ListTasksFilter,
15
- } from '../types.js';
16
-
17
- /**
18
- * Abstract storage adapter interface.
19
- * Implemented by:
20
- * - FSAdapter (Phase 4): Local filesystem storage
21
- * - GitHubAdapter (Phase 5): GitHub Issues as storage
22
- */
23
- export interface StorageAdapter {
24
- // Team operations
25
- initTeam(name: string, windowId?: string): Promise<Team>;
26
- getTeam(): Promise<Team | null>;
27
- updateTeam(updates: Partial<Team>): Promise<Team>;
28
-
29
- // Milestone operations
30
- createMilestone(input: CreateMilestoneInput): Promise<Milestone>;
31
- getMilestone(id: string): Promise<Milestone | null>;
32
- listMilestones(): Promise<Milestone[]>;
33
- updateMilestone(id: string, input: UpdateMilestoneInput): Promise<Milestone>;
34
- deleteMilestone(id: string): Promise<void>;
35
-
36
- // Task operations
37
- createTask(input: CreateTaskInput): Promise<Task>;
38
- getTask(id: string): Promise<Task | null>;
39
- listTasks(filter?: ListTasksFilter): Promise<Task[]>;
40
- updateTask(id: string, input: UpdateTaskInput): Promise<Task>;
41
- deleteTask(id: string): Promise<void>;
42
-
43
- // Documentation
44
- getTaskDoc(id: string): Promise<string | null>;
45
- setTaskDoc(id: string, content: string): Promise<void>;
46
- getMilestoneDoc(id: string): Promise<string | null>;
47
- setMilestoneDoc(id: string, content: string): Promise<void>;
48
-
49
- // Audit log
50
- appendEvent(event: AuditEvent): Promise<void>;
51
- getEvents(limit?: number): Promise<AuditEvent[]>;
52
- }
53
-
54
- /**
55
- * Factory function type for creating storage adapters.
56
- */
57
- export type StorageAdapterFactory = (teamDir: string) => StorageAdapter;