aegis-bridge 2.15.1 → 2.15.3
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 +9 -0
- package/dist/api-contracts.d.ts +2 -0
- package/dist/consensus.d.ts +16 -0
- package/dist/consensus.js +19 -0
- package/dist/hooks.js +32 -0
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.js +67 -0
- package/dist/path-utils.d.ts +11 -0
- package/dist/path-utils.js +21 -0
- package/dist/permission-evaluator.d.ts +10 -0
- package/dist/permission-evaluator.js +48 -0
- package/dist/pipeline.d.ts +11 -0
- package/dist/pipeline.js +38 -0
- package/dist/server.js +71 -3
- package/dist/session.d.ts +5 -1
- package/dist/session.js +4 -1
- package/dist/tmux.js +2 -1
- package/dist/validation.d.ts +63 -0
- package/dist/validation.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<img src="https://img.shields.io/github/actions/workflow/status/OneStepAt4time/aegis/ci.yml?branch=main" alt="CI" />
|
|
8
8
|
<img src="https://img.shields.io/npm/l/aegis-bridge.svg" alt="license" />
|
|
9
9
|
<img src="https://img.shields.io/badge/node-%3E%3D20.0.0-blue.svg" alt="node" />
|
|
10
|
+
<img src="https://img.shields.io/badge/MCP-ready-green.svg" alt="MCP ready" />
|
|
10
11
|
</p>
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
@@ -96,6 +97,14 @@ Or via `.mcp.json`:
|
|
|
96
97
|
|
|
97
98
|
**3 prompts** — `implement_issue`, `review_pr`, `debug_session`
|
|
98
99
|
|
|
100
|
+
## Ecosystem Integrations
|
|
101
|
+
|
|
102
|
+
Aegis works beyond Claude Code anywhere an MCP host can launch a local stdio server.
|
|
103
|
+
|
|
104
|
+
- [Cursor integration](docs/integrations/cursor.md)
|
|
105
|
+
- [Windsurf integration](docs/integrations/windsurf.md)
|
|
106
|
+
- [MCP Registry preparation](docs/integrations/mcp-registry.md)
|
|
107
|
+
|
|
99
108
|
---
|
|
100
109
|
|
|
101
110
|
## REST API
|
package/dist/api-contracts.d.ts
CHANGED
|
@@ -157,6 +157,7 @@ export interface CreateSessionRequest {
|
|
|
157
157
|
workDir: string;
|
|
158
158
|
name?: string;
|
|
159
159
|
prompt?: string;
|
|
160
|
+
prd?: string;
|
|
160
161
|
resumeSessionId?: string;
|
|
161
162
|
claudeCommand?: string;
|
|
162
163
|
env?: Record<string, string>;
|
|
@@ -182,6 +183,7 @@ export interface SessionSummary {
|
|
|
182
183
|
createdAt: number;
|
|
183
184
|
lastActivity: number;
|
|
184
185
|
permissionMode: string;
|
|
186
|
+
prd?: string;
|
|
185
187
|
}
|
|
186
188
|
export interface OkResponse {
|
|
187
189
|
ok: boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ConsensusFocusArea = 'correctness' | 'security' | 'performance';
|
|
2
|
+
export interface ConsensusRequest {
|
|
3
|
+
id: string;
|
|
4
|
+
targetSessionId: string;
|
|
5
|
+
reviewerIds: string[];
|
|
6
|
+
focusAreas: ConsensusFocusArea[];
|
|
7
|
+
status: 'running' | 'completed' | 'failed';
|
|
8
|
+
createdAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ConsensusReview {
|
|
11
|
+
reviewerId: string;
|
|
12
|
+
focusArea: ConsensusFocusArea;
|
|
13
|
+
findings: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function buildConsensusPrompt(targetSessionId: string, focusArea: ConsensusFocusArea): string;
|
|
16
|
+
export declare function mergeConsensusFindings(reviews: ConsensusReview[]): string[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function buildConsensusPrompt(targetSessionId, focusArea) {
|
|
2
|
+
return [
|
|
3
|
+
`Review Aegis session ${targetSessionId}.`,
|
|
4
|
+
`Focus area: ${focusArea}.`,
|
|
5
|
+
'Return concise findings ordered by severity.',
|
|
6
|
+
'Prefer concrete regressions, risks, and missing verification.',
|
|
7
|
+
].join(' ');
|
|
8
|
+
}
|
|
9
|
+
export function mergeConsensusFindings(reviews) {
|
|
10
|
+
const merged = new Set();
|
|
11
|
+
for (const review of reviews) {
|
|
12
|
+
for (const finding of review.findings) {
|
|
13
|
+
const normalized = finding.trim();
|
|
14
|
+
if (normalized)
|
|
15
|
+
merged.add(normalized);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return Array.from(merged.values());
|
|
19
|
+
}
|
package/dist/hooks.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Issue #169: Phase 3 — Hook-driven status detection.
|
|
16
16
|
*/
|
|
17
17
|
import { isValidUUID, hookBodySchema, parseIntSafe } from './validation.js';
|
|
18
|
+
import { evaluatePermissionProfile } from './permission-evaluator.js';
|
|
18
19
|
/** CC hook events that require a decision response. */
|
|
19
20
|
const DECISION_EVENTS = new Set(['PreToolUse', 'PermissionRequest']);
|
|
20
21
|
/** Permission modes that should be auto-approved via hook response. */
|
|
@@ -289,6 +290,37 @@ export function registerHookRoutes(app, deps) {
|
|
|
289
290
|
// Timeout: allow without answer (CC shows question to user in terminal)
|
|
290
291
|
console.log(`Hooks: AskUserQuestion timeout for session ${sessionId} — allowing without answer`);
|
|
291
292
|
}
|
|
293
|
+
if (session.permissionProfile) {
|
|
294
|
+
const evaluation = evaluatePermissionProfile(session.permissionProfile, {
|
|
295
|
+
toolName,
|
|
296
|
+
toolInput: hookBody.tool_input,
|
|
297
|
+
});
|
|
298
|
+
if (evaluation.behavior === 'deny') {
|
|
299
|
+
deps.eventBus.emit(sessionId, {
|
|
300
|
+
event: 'permission_denied',
|
|
301
|
+
sessionId,
|
|
302
|
+
timestamp: new Date().toISOString(),
|
|
303
|
+
data: { toolName, reason: evaluation.reason },
|
|
304
|
+
});
|
|
305
|
+
return reply.status(200).send({
|
|
306
|
+
hookSpecificOutput: {
|
|
307
|
+
hookEventName: 'PreToolUse',
|
|
308
|
+
permissionDecision: 'deny',
|
|
309
|
+
reason: evaluation.reason,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (evaluation.behavior === 'ask') {
|
|
314
|
+
deps.eventBus.emitApproval(sessionId, `Permission profile requires approval for ${toolName}`);
|
|
315
|
+
const decision = await deps.sessions.waitForPermissionDecision(sessionId, PERMISSION_TIMEOUT_MS, toolName, evaluation.reason);
|
|
316
|
+
return reply.status(200).send({
|
|
317
|
+
hookSpecificOutput: {
|
|
318
|
+
hookEventName: 'PreToolUse',
|
|
319
|
+
permissionDecision: decision,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
292
324
|
// Default: allow without modification
|
|
293
325
|
return reply.status(200).send({
|
|
294
326
|
hookSpecificOutput: {
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -57,6 +57,16 @@ interface SessionLatencyResponse {
|
|
|
57
57
|
realtime: SessionLatency | null;
|
|
58
58
|
aggregated: SessionLatencySummary | null;
|
|
59
59
|
}
|
|
60
|
+
interface MemoryEntryResponse {
|
|
61
|
+
entry: {
|
|
62
|
+
key: string;
|
|
63
|
+
value: string;
|
|
64
|
+
namespace: string;
|
|
65
|
+
created_at: number;
|
|
66
|
+
updated_at: number;
|
|
67
|
+
expires_at?: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
60
70
|
export declare class AegisClient {
|
|
61
71
|
private baseUrl;
|
|
62
72
|
private authToken?;
|
|
@@ -103,6 +113,9 @@ export declare class AegisClient {
|
|
|
103
113
|
}>;
|
|
104
114
|
}): Promise<PipelineState>;
|
|
105
115
|
getSwarm(): Promise<Record<string, unknown>>;
|
|
116
|
+
setMemory(key: string, value: string, ttlSeconds?: number): Promise<MemoryEntryResponse>;
|
|
117
|
+
getMemory(key: string): Promise<MemoryEntryResponse>;
|
|
118
|
+
deleteMemory(key: string): Promise<OkResponse>;
|
|
106
119
|
}
|
|
107
120
|
export declare function createMcpServer(aegisPort: number, authToken?: string): McpServer;
|
|
108
121
|
export declare function startMcpServer(port: number, authToken?: string): Promise<void>;
|
package/dist/mcp-server.js
CHANGED
|
@@ -175,6 +175,20 @@ export class AegisClient {
|
|
|
175
175
|
async getSwarm() {
|
|
176
176
|
return this.request('/v1/swarm');
|
|
177
177
|
}
|
|
178
|
+
async setMemory(key, value, ttlSeconds) {
|
|
179
|
+
return this.request('/v1/memory', {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
body: JSON.stringify({ key, value, ttlSeconds }),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async getMemory(key) {
|
|
185
|
+
return this.request(`/v1/memory/${encodeURIComponent(key)}`);
|
|
186
|
+
}
|
|
187
|
+
async deleteMemory(key) {
|
|
188
|
+
return this.request(`/v1/memory/${encodeURIComponent(key)}`, {
|
|
189
|
+
method: 'DELETE',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
178
192
|
}
|
|
179
193
|
function formatToolError(e) {
|
|
180
194
|
if (e instanceof Error) {
|
|
@@ -665,6 +679,59 @@ export function createMcpServer(aegisPort, authToken) {
|
|
|
665
679
|
return formatToolError(e);
|
|
666
680
|
}
|
|
667
681
|
});
|
|
682
|
+
// ── state_set ──
|
|
683
|
+
server.tool('state_set', 'Set a shared state key/value entry via Aegis memory bridge.', {
|
|
684
|
+
key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
|
|
685
|
+
value: z.string().describe('State payload as string'),
|
|
686
|
+
ttlSeconds: z.number().int().positive().max(86400 * 30).optional().describe('Optional TTL in seconds (max 30 days)'),
|
|
687
|
+
}, async ({ key, value, ttlSeconds }) => {
|
|
688
|
+
try {
|
|
689
|
+
const result = await client.setMemory(key, value, ttlSeconds);
|
|
690
|
+
return {
|
|
691
|
+
content: [{
|
|
692
|
+
type: 'text',
|
|
693
|
+
text: JSON.stringify(result, null, 2),
|
|
694
|
+
}],
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
catch (e) {
|
|
698
|
+
return formatToolError(e);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// ── state_get ──
|
|
702
|
+
server.tool('state_get', 'Get a shared state key/value entry via Aegis memory bridge.', {
|
|
703
|
+
key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
|
|
704
|
+
}, async ({ key }) => {
|
|
705
|
+
try {
|
|
706
|
+
const result = await client.getMemory(key);
|
|
707
|
+
return {
|
|
708
|
+
content: [{
|
|
709
|
+
type: 'text',
|
|
710
|
+
text: JSON.stringify(result, null, 2),
|
|
711
|
+
}],
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
catch (e) {
|
|
715
|
+
return formatToolError(e);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
// ── state_delete ──
|
|
719
|
+
server.tool('state_delete', 'Delete a shared state key/value entry via Aegis memory bridge.', {
|
|
720
|
+
key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
|
|
721
|
+
}, async ({ key }) => {
|
|
722
|
+
try {
|
|
723
|
+
const result = await client.deleteMemory(key);
|
|
724
|
+
return {
|
|
725
|
+
content: [{
|
|
726
|
+
type: 'text',
|
|
727
|
+
text: JSON.stringify(result, null, 2),
|
|
728
|
+
}],
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
return formatToolError(e);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
668
735
|
// ── MCP Prompts (Issue #443) ────────────────────────────────────────
|
|
669
736
|
server.prompt('implement_issue', 'Create a session and generate a structured implementation prompt for a GitHub issue.', {
|
|
670
737
|
issueNumber: z.string().describe('GitHub issue number'),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-utils.ts — path helpers shared across session/tmux logic.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Compute the Claude project hash folder from a workDir path.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - /home/user/project -> -home-user-project
|
|
9
|
+
* - D:\\Users\\me\\project -> -d-Users-me-project
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeProjectHash(workDir: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-utils.ts — path helpers shared across session/tmux logic.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Compute the Claude project hash folder from a workDir path.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - /home/user/project -> -home-user-project
|
|
9
|
+
* - D:\\Users\\me\\project -> -d-Users-me-project
|
|
10
|
+
*/
|
|
11
|
+
export function computeProjectHash(workDir) {
|
|
12
|
+
const normalized = workDir.replace(/\\/g, '/').trim();
|
|
13
|
+
const withLowerDrive = normalized.replace(/^[A-Za-z]:/, (m) => `${m[0].toLowerCase()}`);
|
|
14
|
+
const segments = withLowerDrive
|
|
15
|
+
.split('/')
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((segment) => segment.replace(/:/g, '').replace(/\s+/g, '-'));
|
|
18
|
+
if (segments.length === 0)
|
|
19
|
+
return '-';
|
|
20
|
+
return `-${segments.join('-')}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PermissionProfile } from './validation.js';
|
|
2
|
+
export interface PermissionEvaluationInput {
|
|
3
|
+
toolName: string;
|
|
4
|
+
toolInput?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface PermissionEvaluationResult {
|
|
7
|
+
behavior: 'allow' | 'deny' | 'ask';
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function evaluatePermissionProfile(profile: PermissionProfile, input: PermissionEvaluationInput): PermissionEvaluationResult;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function globToRegExp(pattern) {
|
|
2
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
3
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
4
|
+
}
|
|
5
|
+
function extractCandidatePaths(toolInput) {
|
|
6
|
+
if (!toolInput)
|
|
7
|
+
return [];
|
|
8
|
+
const values = [toolInput.path, toolInput.file_path, toolInput.target, ...(Array.isArray(toolInput.paths) ? toolInput.paths : [])];
|
|
9
|
+
return values.filter((v) => typeof v === 'string');
|
|
10
|
+
}
|
|
11
|
+
function extractContentSize(toolInput) {
|
|
12
|
+
const content = toolInput?.content;
|
|
13
|
+
return typeof content === 'string' ? content.length : null;
|
|
14
|
+
}
|
|
15
|
+
function isLikelyWriteTool(toolName) {
|
|
16
|
+
return /write|edit|delete|rename|move|create/i.test(toolName);
|
|
17
|
+
}
|
|
18
|
+
export function evaluatePermissionProfile(profile, input) {
|
|
19
|
+
for (const rule of profile.rules) {
|
|
20
|
+
if (rule.tool !== input.toolName)
|
|
21
|
+
continue;
|
|
22
|
+
if (rule.pattern) {
|
|
23
|
+
const candidate = typeof input.toolInput?.command === 'string'
|
|
24
|
+
? input.toolInput.command
|
|
25
|
+
: JSON.stringify(input.toolInput ?? {});
|
|
26
|
+
if (!globToRegExp(rule.pattern).test(candidate))
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (rule.constraints?.readOnly && isLikelyWriteTool(input.toolName)) {
|
|
30
|
+
return { behavior: 'deny', reason: `Denied by readOnly constraint for ${input.toolName}` };
|
|
31
|
+
}
|
|
32
|
+
if (rule.constraints?.paths && rule.constraints.paths.length > 0) {
|
|
33
|
+
const paths = extractCandidatePaths(input.toolInput);
|
|
34
|
+
const allowed = paths.every((candidate) => rule.constraints.paths.some((prefix) => candidate.startsWith(prefix)));
|
|
35
|
+
if (!allowed) {
|
|
36
|
+
return { behavior: 'deny', reason: `Denied by path constraint for ${input.toolName}` };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (rule.constraints?.maxFileSize) {
|
|
40
|
+
const size = extractContentSize(input.toolInput);
|
|
41
|
+
if (size !== null && size > rule.constraints.maxFileSize) {
|
|
42
|
+
return { behavior: 'deny', reason: `Denied by maxFileSize constraint for ${input.toolName}` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { behavior: rule.behavior, reason: `Matched rule for ${input.toolName}` };
|
|
46
|
+
}
|
|
47
|
+
return { behavior: profile.defaultBehavior, reason: 'No matching permission rule' };
|
|
48
|
+
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -46,7 +46,16 @@ export type PipelineStageStatus = 'pending' | 'running' | 'completed' | 'failed'
|
|
|
46
46
|
export interface PipelineState {
|
|
47
47
|
id: string;
|
|
48
48
|
name: string;
|
|
49
|
+
currentStage: 'plan' | 'execute' | 'verify' | 'fix' | 'submit' | 'done';
|
|
49
50
|
status: 'running' | 'completed' | 'failed';
|
|
51
|
+
retryCount: number;
|
|
52
|
+
maxRetries: number;
|
|
53
|
+
stageHistory: Array<{
|
|
54
|
+
stage: string;
|
|
55
|
+
enteredAt: number;
|
|
56
|
+
exitedAt?: number;
|
|
57
|
+
output?: unknown;
|
|
58
|
+
}>;
|
|
50
59
|
stages: Array<{
|
|
51
60
|
name: string;
|
|
52
61
|
status: PipelineStageStatus;
|
|
@@ -62,6 +71,7 @@ export declare class PipelineManager {
|
|
|
62
71
|
private sessions;
|
|
63
72
|
private eventBus?;
|
|
64
73
|
private static readonly PIPELINE_RETRY_MAX_ATTEMPTS;
|
|
74
|
+
private static readonly PIPELINE_FIX_MAX_RETRIES;
|
|
65
75
|
private pipelines;
|
|
66
76
|
private pipelineConfigs;
|
|
67
77
|
private pollInterval;
|
|
@@ -78,6 +88,7 @@ export declare class PipelineManager {
|
|
|
78
88
|
private advancePipeline;
|
|
79
89
|
/** Poll running pipelines and advance stages. */
|
|
80
90
|
private pollPipelines;
|
|
91
|
+
private transitionPipelineStage;
|
|
81
92
|
/** Detect circular dependencies. Throws if found. */
|
|
82
93
|
private detectCycles;
|
|
83
94
|
/** Clean up. */
|
package/dist/pipeline.js
CHANGED
|
@@ -11,6 +11,7 @@ export class PipelineManager {
|
|
|
11
11
|
sessions;
|
|
12
12
|
eventBus;
|
|
13
13
|
static PIPELINE_RETRY_MAX_ATTEMPTS = 3;
|
|
14
|
+
static PIPELINE_FIX_MAX_RETRIES = 3;
|
|
14
15
|
pipelines = new Map();
|
|
15
16
|
pipelineConfigs = new Map(); // #219: preserve original stage config
|
|
16
17
|
pollInterval = null;
|
|
@@ -72,7 +73,11 @@ export class PipelineManager {
|
|
|
72
73
|
const pipeline = {
|
|
73
74
|
id,
|
|
74
75
|
name: config.name,
|
|
76
|
+
currentStage: 'plan',
|
|
75
77
|
status: 'running',
|
|
78
|
+
retryCount: 0,
|
|
79
|
+
maxRetries: PipelineManager.PIPELINE_FIX_MAX_RETRIES,
|
|
80
|
+
stageHistory: [{ stage: 'plan', enteredAt: Date.now() }],
|
|
76
81
|
stages: config.stages.map(s => ({
|
|
77
82
|
name: s.name,
|
|
78
83
|
status: 'pending',
|
|
@@ -108,11 +113,14 @@ export class PipelineManager {
|
|
|
108
113
|
// If any stage failed, fail the pipeline
|
|
109
114
|
if (failedStages.length > 0) {
|
|
110
115
|
pipeline.status = 'failed';
|
|
116
|
+
this.transitionPipelineStage(pipeline, 'fix', { reason: 'stage_failed', failedStages: failedStages.map(s => s.name) });
|
|
111
117
|
return;
|
|
112
118
|
}
|
|
113
119
|
// Check if all stages are completed
|
|
114
120
|
if (pipeline.stages.every(s => s.status === 'completed')) {
|
|
115
121
|
pipeline.status = 'completed';
|
|
122
|
+
this.transitionPipelineStage(pipeline, 'submit', { reason: 'all_stages_completed' });
|
|
123
|
+
this.transitionPipelineStage(pipeline, 'done', { status: 'completed' });
|
|
116
124
|
if (this.eventBus) {
|
|
117
125
|
this.eventBus.emitEnded(id, 'pipeline_completed');
|
|
118
126
|
}
|
|
@@ -148,13 +156,23 @@ export class PipelineManager {
|
|
|
148
156
|
stage.sessionId = session.id;
|
|
149
157
|
stage.status = 'running';
|
|
150
158
|
stage.startedAt = Date.now();
|
|
159
|
+
this.transitionPipelineStage(pipeline, 'execute', { stage: stage.name, sessionId: session.id });
|
|
151
160
|
}
|
|
152
161
|
catch (e) {
|
|
153
162
|
stage.status = 'failed';
|
|
154
163
|
stage.error = getErrorMessage(e);
|
|
155
164
|
pipeline.status = 'failed';
|
|
165
|
+
this.transitionPipelineStage(pipeline, 'fix', { stage: stage.name, error: stage.error });
|
|
156
166
|
}
|
|
157
167
|
}
|
|
168
|
+
const hasRunning = pipeline.stages.some(s => s.status === 'running');
|
|
169
|
+
const hasPending = pipeline.stages.some(s => s.status === 'pending');
|
|
170
|
+
if (hasRunning) {
|
|
171
|
+
this.transitionPipelineStage(pipeline, 'verify', { runningStages: pipeline.stages.filter(s => s.status === 'running').map(s => s.name) });
|
|
172
|
+
}
|
|
173
|
+
else if (hasPending) {
|
|
174
|
+
this.transitionPipelineStage(pipeline, 'plan', { pendingStages: pipeline.stages.filter(s => s.status === 'pending').map(s => s.name) });
|
|
175
|
+
}
|
|
158
176
|
}
|
|
159
177
|
/** Poll running pipelines and advance stages. */
|
|
160
178
|
async pollPipelines() {
|
|
@@ -184,6 +202,7 @@ export class PipelineManager {
|
|
|
184
202
|
if (session.status === 'idle') {
|
|
185
203
|
stage.status = 'completed';
|
|
186
204
|
stage.completedAt = Date.now();
|
|
205
|
+
this.transitionPipelineStage(pipeline, 'verify', { stageCompleted: stage.name });
|
|
187
206
|
}
|
|
188
207
|
}
|
|
189
208
|
// #219: Use stored original config so stage prompt/permissionMode/autoApprove/workDir are preserved
|
|
@@ -207,6 +226,25 @@ export class PipelineManager {
|
|
|
207
226
|
}
|
|
208
227
|
}
|
|
209
228
|
}
|
|
229
|
+
transitionPipelineStage(pipeline, stage, output) {
|
|
230
|
+
if (pipeline.currentStage === stage)
|
|
231
|
+
return;
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const previous = pipeline.stageHistory[pipeline.stageHistory.length - 1];
|
|
234
|
+
if (previous && previous.exitedAt === undefined) {
|
|
235
|
+
previous.exitedAt = now;
|
|
236
|
+
if (output !== undefined)
|
|
237
|
+
previous.output = output;
|
|
238
|
+
}
|
|
239
|
+
pipeline.currentStage = stage;
|
|
240
|
+
pipeline.stageHistory.push({ stage, enteredAt: now });
|
|
241
|
+
if (stage === 'fix') {
|
|
242
|
+
pipeline.retryCount += 1;
|
|
243
|
+
if (pipeline.retryCount > pipeline.maxRetries) {
|
|
244
|
+
pipeline.status = 'failed';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
210
248
|
/** Detect circular dependencies. Throws if found. */
|
|
211
249
|
detectCycles(stages) {
|
|
212
250
|
const graph = new Map();
|
package/dist/server.js
CHANGED
|
@@ -37,6 +37,7 @@ import { registerHookRoutes } from './hooks.js';
|
|
|
37
37
|
import { registerWsTerminalRoute } from './ws-terminal.js';
|
|
38
38
|
import { registerMemoryRoutes } from './memory-routes.js';
|
|
39
39
|
import { registerModelRouterRoutes } from './model-router.js';
|
|
40
|
+
import { buildConsensusPrompt } from './consensus.js';
|
|
40
41
|
import * as templateStore from './template-store.js';
|
|
41
42
|
import { SwarmMonitor } from './swarm-monitor.js';
|
|
42
43
|
import { killAllSessions } from './signal-cleanup-helper.js';
|
|
@@ -48,9 +49,10 @@ import { MemoryBridge } from './memory-bridge.js';
|
|
|
48
49
|
import { cleanupTerminatedSessionState } from './session-cleanup.js';
|
|
49
50
|
import { normalizeApiErrorPayload } from './api-error-envelope.js';
|
|
50
51
|
import { listenWithRetry, removePidFile, writePidFile } from './startup.js';
|
|
51
|
-
import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, compareSemver, extractCCVersion, MIN_CC_VERSION, } from './validation.js';
|
|
52
|
+
import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, compareSemver, extractCCVersion, MIN_CC_VERSION, permissionProfileSchema, } from './validation.js';
|
|
52
53
|
const __filename = fileURLToPath(import.meta.url);
|
|
53
54
|
const __dirname = path.dirname(__filename);
|
|
55
|
+
const consensusRequests = new Map();
|
|
54
56
|
// ── Configuration ────────────────────────────────────────────────────
|
|
55
57
|
// Issue #349: CSP policy for dashboard responses (shared between static and SPA fallback)
|
|
56
58
|
const DASHBOARD_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:";
|
|
@@ -349,6 +351,7 @@ const createSessionSchema = z.object({
|
|
|
349
351
|
workDir: z.string().min(1),
|
|
350
352
|
name: z.string().max(200).optional(),
|
|
351
353
|
prompt: z.string().max(100_000).optional(),
|
|
354
|
+
prd: z.string().max(100_000).optional(),
|
|
352
355
|
resumeSessionId: z.string().uuid().optional(),
|
|
353
356
|
claudeCommand: z.string().max(10_000).optional(),
|
|
354
357
|
env: z.record(z.string(), z.string()).optional(),
|
|
@@ -680,7 +683,7 @@ async function createSessionHandler(req, reply) {
|
|
|
680
683
|
if (!parsed.success) {
|
|
681
684
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
682
685
|
}
|
|
683
|
-
const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId, memoryKeys } = parsed.data;
|
|
686
|
+
const { workDir, name, prompt, prd, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId, memoryKeys } = parsed.data;
|
|
684
687
|
if (!workDir)
|
|
685
688
|
return reply.status(400).send({ error: 'workDir is required' });
|
|
686
689
|
// Issue #564: Validate installed Claude Code version
|
|
@@ -729,7 +732,7 @@ async function createSessionHandler(req, reply) {
|
|
|
729
732
|
}
|
|
730
733
|
}
|
|
731
734
|
console.time("POST_CREATE_SESSION");
|
|
732
|
-
const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId });
|
|
735
|
+
const session = await sessions.createSession({ workDir: safeWorkDir, name, prd, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId });
|
|
733
736
|
console.timeEnd("POST_CREATE_SESSION");
|
|
734
737
|
console.time("POST_CHANNEL_CREATED");
|
|
735
738
|
// Issue #625: Track session in metrics so sessionsCreated counter is accurate
|
|
@@ -903,6 +906,48 @@ async function forkSessionHandler(req, reply) {
|
|
|
903
906
|
}
|
|
904
907
|
app.post('/v1/sessions/:id/fork', forkSessionHandler);
|
|
905
908
|
app.post('/sessions/:id/fork', forkSessionHandler);
|
|
909
|
+
async function createConsensusHandler(req, reply) {
|
|
910
|
+
const targetSessionId = req.params.id;
|
|
911
|
+
const target = sessions.getSession(targetSessionId);
|
|
912
|
+
if (!target)
|
|
913
|
+
return reply.status(404).send({ error: 'Target session not found' });
|
|
914
|
+
const focusAreas = (req.body?.focusAreas && req.body.focusAreas.length > 0)
|
|
915
|
+
? req.body.focusAreas
|
|
916
|
+
: ['correctness', 'security', 'performance'];
|
|
917
|
+
const reviewerCount = Math.min(5, Math.max(1, req.body?.reviewerCount ?? focusAreas.length));
|
|
918
|
+
const selectedFocus = focusAreas.slice(0, reviewerCount);
|
|
919
|
+
const reviewerIds = [];
|
|
920
|
+
for (let i = 0; i < selectedFocus.length; i += 1) {
|
|
921
|
+
const focus = selectedFocus[i];
|
|
922
|
+
const child = await sessions.createSession({
|
|
923
|
+
workDir: target.workDir,
|
|
924
|
+
name: `consensus-${focus}-${targetSessionId.slice(0, 6)}`,
|
|
925
|
+
parentId: targetSessionId,
|
|
926
|
+
permissionMode: target.permissionMode,
|
|
927
|
+
});
|
|
928
|
+
reviewerIds.push(child.id);
|
|
929
|
+
await sessions.sendInitialPrompt(child.id, buildConsensusPrompt(targetSessionId, focus));
|
|
930
|
+
}
|
|
931
|
+
const consensusId = crypto.randomUUID();
|
|
932
|
+
const record = {
|
|
933
|
+
id: consensusId,
|
|
934
|
+
targetSessionId,
|
|
935
|
+
reviewerIds,
|
|
936
|
+
focusAreas: selectedFocus,
|
|
937
|
+
status: 'running',
|
|
938
|
+
createdAt: Date.now(),
|
|
939
|
+
};
|
|
940
|
+
consensusRequests.set(consensusId, record);
|
|
941
|
+
return reply.status(202).send(record);
|
|
942
|
+
}
|
|
943
|
+
function getConsensusHandler(req, reply) {
|
|
944
|
+
const item = consensusRequests.get(req.params.id);
|
|
945
|
+
if (!item)
|
|
946
|
+
return reply.status(404).send({ error: 'Consensus request not found' });
|
|
947
|
+
return item;
|
|
948
|
+
}
|
|
949
|
+
app.post('/v1/sessions/:id/consensus', createConsensusHandler);
|
|
950
|
+
app.get('/v1/consensus/:id', getConsensusHandler);
|
|
906
951
|
async function getPermissionPolicyHandler(req, reply) {
|
|
907
952
|
const sessionId = req.params.id;
|
|
908
953
|
const session = sessions.getSession(sessionId);
|
|
@@ -927,6 +972,29 @@ app.get('/v1/sessions/:id/permissions', getPermissionPolicyHandler);
|
|
|
927
972
|
app.put('/v1/sessions/:id/permissions', updatePermissionPolicyHandler);
|
|
928
973
|
app.get('/sessions/:id/permissions', getPermissionPolicyHandler);
|
|
929
974
|
app.put('/sessions/:id/permissions', updatePermissionPolicyHandler);
|
|
975
|
+
async function getPermissionProfileHandler(req, reply) {
|
|
976
|
+
const sessionId = req.params.id;
|
|
977
|
+
const session = sessions.getSession(sessionId);
|
|
978
|
+
if (!session)
|
|
979
|
+
return reply.status(404).send({ error: 'Session not found' });
|
|
980
|
+
return { permissionProfile: session.permissionProfile ?? null };
|
|
981
|
+
}
|
|
982
|
+
async function updatePermissionProfileHandler(req, reply) {
|
|
983
|
+
const sessionId = req.params.id;
|
|
984
|
+
const session = sessions.getSession(sessionId);
|
|
985
|
+
if (!session)
|
|
986
|
+
return reply.status(404).send({ error: 'Session not found' });
|
|
987
|
+
const parsed = permissionProfileSchema.safeParse(req.body ?? {});
|
|
988
|
+
if (!parsed.success)
|
|
989
|
+
return reply.status(400).send({ error: 'Invalid permission profile', details: parsed.error.issues });
|
|
990
|
+
session.permissionProfile = parsed.data;
|
|
991
|
+
await sessions.save();
|
|
992
|
+
return { permissionProfile: parsed.data };
|
|
993
|
+
}
|
|
994
|
+
app.get('/v1/sessions/:id/permission-profile', getPermissionProfileHandler);
|
|
995
|
+
app.put('/v1/sessions/:id/permission-profile', updatePermissionProfileHandler);
|
|
996
|
+
app.get('/sessions/:id/permission-profile', getPermissionProfileHandler);
|
|
997
|
+
app.put('/sessions/:id/permission-profile', updatePermissionProfileHandler);
|
|
930
998
|
// Read messages
|
|
931
999
|
async function readMessagesHandler(req, reply) {
|
|
932
1000
|
try {
|
package/dist/session.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { TmuxManager } from './tmux.js';
|
|
|
8
8
|
import { type ParsedEntry } from './transcript.js';
|
|
9
9
|
import { type UIState } from './terminal-parser.js';
|
|
10
10
|
import type { Config } from './config.js';
|
|
11
|
-
import { type PermissionPolicy } from './validation.js';
|
|
11
|
+
import { type PermissionPolicy, type PermissionProfile } from './validation.js';
|
|
12
12
|
import { type PermissionDecision } from './permission-request-manager.js';
|
|
13
13
|
export interface SessionInfo {
|
|
14
14
|
id: string;
|
|
@@ -40,6 +40,8 @@ export interface SessionInfo {
|
|
|
40
40
|
parentId?: string;
|
|
41
41
|
children?: string[];
|
|
42
42
|
permissionPolicy?: PermissionPolicy;
|
|
43
|
+
permissionProfile?: PermissionProfile;
|
|
44
|
+
prd?: string;
|
|
43
45
|
}
|
|
44
46
|
export interface SessionState {
|
|
45
47
|
sessions: Record<string, SessionInfo>;
|
|
@@ -135,6 +137,7 @@ export declare class SessionManager {
|
|
|
135
137
|
createSession(opts: {
|
|
136
138
|
workDir: string;
|
|
137
139
|
name?: string;
|
|
140
|
+
prd?: string;
|
|
138
141
|
resumeSessionId?: string;
|
|
139
142
|
claudeCommand?: string;
|
|
140
143
|
env?: Record<string, string>;
|
|
@@ -304,6 +307,7 @@ export declare class SessionManager {
|
|
|
304
307
|
createdAt: number;
|
|
305
308
|
lastActivity: number;
|
|
306
309
|
permissionMode: string;
|
|
310
|
+
prd?: string;
|
|
307
311
|
}>;
|
|
308
312
|
/** Paginated transcript read — does NOT advance the session's byteOffset. */
|
|
309
313
|
readTranscript(id: string, page?: number, limit?: number, roleFilter?: 'user' | 'assistant' | 'system'): Promise<{
|
package/dist/session.js
CHANGED
|
@@ -21,6 +21,7 @@ import { PermissionRequestManager } from './permission-request-manager.js';
|
|
|
21
21
|
import { QuestionManager } from './question-manager.js';
|
|
22
22
|
import { Mutex } from 'async-mutex';
|
|
23
23
|
import { maybeInjectFault } from './fault-injection.js';
|
|
24
|
+
import { computeProjectHash } from './path-utils.js';
|
|
24
25
|
/** Convert parsed JSON arrays to Sets for activeSubagents (#668). */
|
|
25
26
|
function hydrateSessions(raw) {
|
|
26
27
|
const sessions = {};
|
|
@@ -562,6 +563,7 @@ export class SessionManager {
|
|
|
562
563
|
settingsPatched,
|
|
563
564
|
hookSettingsFile,
|
|
564
565
|
hookSecret,
|
|
566
|
+
prd: opts.prd,
|
|
565
567
|
};
|
|
566
568
|
this.state.sessions[id] = session;
|
|
567
569
|
this.invalidateSessionsListCache();
|
|
@@ -1207,6 +1209,7 @@ export class SessionManager {
|
|
|
1207
1209
|
createdAt: session.createdAt,
|
|
1208
1210
|
lastActivity: session.lastActivity,
|
|
1209
1211
|
permissionMode: session.permissionMode,
|
|
1212
|
+
prd: session.prd,
|
|
1210
1213
|
};
|
|
1211
1214
|
}
|
|
1212
1215
|
/** Paginated transcript read — does NOT advance the session's byteOffset. */
|
|
@@ -1400,7 +1403,7 @@ export class SessionManager {
|
|
|
1400
1403
|
}
|
|
1401
1404
|
/** Attempt filesystem-based discovery for a single session poll tick. */
|
|
1402
1405
|
async maybeDiscoverFromFilesystem(session, workDir) {
|
|
1403
|
-
const projectHash =
|
|
1406
|
+
const projectHash = computeProjectHash(workDir);
|
|
1404
1407
|
const projectDir = join(this.config.claudeProjectsDir, projectHash);
|
|
1405
1408
|
if (!existsSync(projectDir))
|
|
1406
1409
|
return false;
|
package/dist/tmux.js
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { homedir, tmpdir } from 'node:os';
|
|
13
13
|
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { computeProjectHash } from './path-utils.js';
|
|
14
15
|
/** Shell-escape a string by wrapping in single quotes and escaping embedded single quotes. */
|
|
15
16
|
function shellEscape(s) {
|
|
16
17
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
@@ -360,7 +361,7 @@ export class TmuxManager {
|
|
|
360
361
|
*/
|
|
361
362
|
async archiveStaleSessionFiles(workDir) {
|
|
362
363
|
// Compute the project hash the same way Claude CLI does
|
|
363
|
-
const projectHash =
|
|
364
|
+
const projectHash = computeProjectHash(workDir);
|
|
364
365
|
const projectDir = join(homedir(), '.claude', 'projects', projectHash);
|
|
365
366
|
if (!existsSync(projectDir))
|
|
366
367
|
return;
|
package/dist/validation.d.ts
CHANGED
|
@@ -141,6 +141,48 @@ export declare const permissionRuleSchema: z.ZodObject<{
|
|
|
141
141
|
commandPattern: z.ZodOptional<z.ZodString>;
|
|
142
142
|
}, z.core.$strip>;
|
|
143
143
|
export type PermissionPolicy = z.infer<typeof permissionRuleSchema>[];
|
|
144
|
+
/** Issue #742: richer per-session permission profile. */
|
|
145
|
+
export declare const permissionConstraintSchema: z.ZodObject<{
|
|
146
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
147
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
148
|
+
maxFileSize: z.ZodOptional<z.ZodNumber>;
|
|
149
|
+
}, z.core.$strict>;
|
|
150
|
+
export declare const permissionProfileRuleSchema: z.ZodObject<{
|
|
151
|
+
tool: z.ZodString;
|
|
152
|
+
behavior: z.ZodEnum<{
|
|
153
|
+
allow: "allow";
|
|
154
|
+
deny: "deny";
|
|
155
|
+
ask: "ask";
|
|
156
|
+
}>;
|
|
157
|
+
pattern: z.ZodOptional<z.ZodString>;
|
|
158
|
+
constraints: z.ZodOptional<z.ZodObject<{
|
|
159
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
160
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
161
|
+
maxFileSize: z.ZodOptional<z.ZodNumber>;
|
|
162
|
+
}, z.core.$strict>>;
|
|
163
|
+
}, z.core.$strict>;
|
|
164
|
+
export declare const permissionProfileSchema: z.ZodObject<{
|
|
165
|
+
defaultBehavior: z.ZodEnum<{
|
|
166
|
+
allow: "allow";
|
|
167
|
+
deny: "deny";
|
|
168
|
+
ask: "ask";
|
|
169
|
+
}>;
|
|
170
|
+
rules: z.ZodArray<z.ZodObject<{
|
|
171
|
+
tool: z.ZodString;
|
|
172
|
+
behavior: z.ZodEnum<{
|
|
173
|
+
allow: "allow";
|
|
174
|
+
deny: "deny";
|
|
175
|
+
ask: "ask";
|
|
176
|
+
}>;
|
|
177
|
+
pattern: z.ZodOptional<z.ZodString>;
|
|
178
|
+
constraints: z.ZodOptional<z.ZodObject<{
|
|
179
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
180
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
181
|
+
maxFileSize: z.ZodOptional<z.ZodNumber>;
|
|
182
|
+
}, z.core.$strict>>;
|
|
183
|
+
}, z.core.$strict>>;
|
|
184
|
+
}, z.core.$strict>;
|
|
185
|
+
export type PermissionProfile = z.infer<typeof permissionProfileSchema>;
|
|
144
186
|
/** Schema for persisted SessionState (sessions: { [id]: SessionInfo }). */
|
|
145
187
|
export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
146
188
|
id: z.ZodString;
|
|
@@ -206,6 +248,27 @@ export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<
|
|
|
206
248
|
toolName: z.ZodOptional<z.ZodString>;
|
|
207
249
|
commandPattern: z.ZodOptional<z.ZodString>;
|
|
208
250
|
}, z.core.$strip>>>;
|
|
251
|
+
permissionProfile: z.ZodOptional<z.ZodObject<{
|
|
252
|
+
defaultBehavior: z.ZodEnum<{
|
|
253
|
+
allow: "allow";
|
|
254
|
+
deny: "deny";
|
|
255
|
+
ask: "ask";
|
|
256
|
+
}>;
|
|
257
|
+
rules: z.ZodArray<z.ZodObject<{
|
|
258
|
+
tool: z.ZodString;
|
|
259
|
+
behavior: z.ZodEnum<{
|
|
260
|
+
allow: "allow";
|
|
261
|
+
deny: "deny";
|
|
262
|
+
ask: "ask";
|
|
263
|
+
}>;
|
|
264
|
+
pattern: z.ZodOptional<z.ZodString>;
|
|
265
|
+
constraints: z.ZodOptional<z.ZodObject<{
|
|
266
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
267
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
268
|
+
maxFileSize: z.ZodOptional<z.ZodNumber>;
|
|
269
|
+
}, z.core.$strict>>;
|
|
270
|
+
}, z.core.$strict>>;
|
|
271
|
+
}, z.core.$strict>>;
|
|
209
272
|
}, z.core.$strip>>;
|
|
210
273
|
/** Schema for a single continuation pointer entry in session_map.json (Issue #900). */
|
|
211
274
|
export declare const sessionMapEntrySchema: z.ZodObject<{
|
package/dist/validation.js
CHANGED
|
@@ -138,6 +138,22 @@ export const permissionRuleSchema = z.object({
|
|
|
138
138
|
toolName: z.string().optional(),
|
|
139
139
|
commandPattern: z.string().optional(),
|
|
140
140
|
});
|
|
141
|
+
/** Issue #742: richer per-session permission profile. */
|
|
142
|
+
export const permissionConstraintSchema = z.object({
|
|
143
|
+
readOnly: z.boolean().optional(),
|
|
144
|
+
paths: z.array(z.string().min(1)).max(50).optional(),
|
|
145
|
+
maxFileSize: z.number().int().positive().max(10_000_000).optional(),
|
|
146
|
+
}).strict();
|
|
147
|
+
export const permissionProfileRuleSchema = z.object({
|
|
148
|
+
tool: z.string().min(1),
|
|
149
|
+
behavior: z.enum(['allow', 'deny', 'ask']),
|
|
150
|
+
pattern: z.string().optional(),
|
|
151
|
+
constraints: permissionConstraintSchema.optional(),
|
|
152
|
+
}).strict();
|
|
153
|
+
export const permissionProfileSchema = z.object({
|
|
154
|
+
defaultBehavior: z.enum(['allow', 'deny', 'ask']),
|
|
155
|
+
rules: z.array(permissionProfileRuleSchema).max(100),
|
|
156
|
+
}).strict();
|
|
141
157
|
/** Schema for persisted SessionState (sessions: { [id]: SessionInfo }). */
|
|
142
158
|
export const persistedStateSchema = z.record(z.string(), z.object({
|
|
143
159
|
id: z.string(),
|
|
@@ -173,6 +189,7 @@ export const persistedStateSchema = z.record(z.string(), z.object({
|
|
|
173
189
|
toolName: z.string().optional(),
|
|
174
190
|
commandPattern: z.string().optional(),
|
|
175
191
|
})).optional(),
|
|
192
|
+
permissionProfile: permissionProfileSchema.optional(),
|
|
176
193
|
}));
|
|
177
194
|
/** Schema for a single continuation pointer entry in session_map.json (Issue #900). */
|
|
178
195
|
export const sessionMapEntrySchema = z.object({
|