codepiper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +28 -0
- package/CHANGELOG.md +10 -0
- package/LEGAL_NOTICE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/package.json +90 -0
- package/packages/cli/package.json +13 -0
- package/packages/cli/src/commands/analytics.ts +157 -0
- package/packages/cli/src/commands/attach.ts +299 -0
- package/packages/cli/src/commands/audit.ts +50 -0
- package/packages/cli/src/commands/auth.ts +261 -0
- package/packages/cli/src/commands/daemon.ts +162 -0
- package/packages/cli/src/commands/doctor.ts +303 -0
- package/packages/cli/src/commands/env-set.ts +162 -0
- package/packages/cli/src/commands/hook-forward.ts +268 -0
- package/packages/cli/src/commands/keys.ts +77 -0
- package/packages/cli/src/commands/kill.ts +19 -0
- package/packages/cli/src/commands/logs.ts +419 -0
- package/packages/cli/src/commands/model.ts +172 -0
- package/packages/cli/src/commands/policy-set.ts +185 -0
- package/packages/cli/src/commands/policy.ts +227 -0
- package/packages/cli/src/commands/providers.ts +114 -0
- package/packages/cli/src/commands/resize.ts +34 -0
- package/packages/cli/src/commands/send.ts +184 -0
- package/packages/cli/src/commands/sessions.ts +202 -0
- package/packages/cli/src/commands/slash.ts +92 -0
- package/packages/cli/src/commands/start.ts +243 -0
- package/packages/cli/src/commands/stop.ts +19 -0
- package/packages/cli/src/commands/tail.ts +137 -0
- package/packages/cli/src/commands/workflow.ts +786 -0
- package/packages/cli/src/commands/workspace.ts +127 -0
- package/packages/cli/src/lib/api.ts +78 -0
- package/packages/cli/src/lib/args.ts +72 -0
- package/packages/cli/src/lib/format.ts +93 -0
- package/packages/cli/src/main.ts +563 -0
- package/packages/core/package.json +7 -0
- package/packages/core/src/config.ts +30 -0
- package/packages/core/src/errors.ts +38 -0
- package/packages/core/src/eventBus.ts +56 -0
- package/packages/core/src/eventBusAdapter.ts +143 -0
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/sqliteEventBus.ts +336 -0
- package/packages/core/src/types.ts +63 -0
- package/packages/daemon/package.json +11 -0
- package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
- package/packages/daemon/src/api/authRoutes.ts +344 -0
- package/packages/daemon/src/api/bodyLimit.ts +133 -0
- package/packages/daemon/src/api/envSetRoutes.ts +170 -0
- package/packages/daemon/src/api/gitRoutes.ts +409 -0
- package/packages/daemon/src/api/hooks.ts +588 -0
- package/packages/daemon/src/api/inputPolicy.ts +249 -0
- package/packages/daemon/src/api/notificationRoutes.ts +532 -0
- package/packages/daemon/src/api/policyRoutes.ts +234 -0
- package/packages/daemon/src/api/policySetRoutes.ts +445 -0
- package/packages/daemon/src/api/routeUtils.ts +28 -0
- package/packages/daemon/src/api/routes.ts +1004 -0
- package/packages/daemon/src/api/server.ts +1388 -0
- package/packages/daemon/src/api/settingsRoutes.ts +367 -0
- package/packages/daemon/src/api/sqliteErrors.ts +47 -0
- package/packages/daemon/src/api/stt.ts +143 -0
- package/packages/daemon/src/api/terminalRoutes.ts +200 -0
- package/packages/daemon/src/api/validation.ts +287 -0
- package/packages/daemon/src/api/validationRoutes.ts +174 -0
- package/packages/daemon/src/api/workflowRoutes.ts +567 -0
- package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
- package/packages/daemon/src/api/ws.ts +1588 -0
- package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
- package/packages/daemon/src/auth/authMiddleware.ts +305 -0
- package/packages/daemon/src/auth/authService.ts +496 -0
- package/packages/daemon/src/auth/rateLimiter.ts +137 -0
- package/packages/daemon/src/config/pricing.ts +79 -0
- package/packages/daemon/src/crypto/encryption.ts +196 -0
- package/packages/daemon/src/db/db.ts +2745 -0
- package/packages/daemon/src/db/index.ts +16 -0
- package/packages/daemon/src/db/migrations.ts +182 -0
- package/packages/daemon/src/db/policyDb.ts +349 -0
- package/packages/daemon/src/db/schema.sql +408 -0
- package/packages/daemon/src/db/workflowDb.ts +464 -0
- package/packages/daemon/src/git/gitUtils.ts +544 -0
- package/packages/daemon/src/index.ts +6 -0
- package/packages/daemon/src/main.ts +525 -0
- package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
- package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
- package/packages/daemon/src/providers/registry.ts +111 -0
- package/packages/daemon/src/providers/types.ts +82 -0
- package/packages/daemon/src/sessions/auditLogger.ts +103 -0
- package/packages/daemon/src/sessions/policyEngine.ts +165 -0
- package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
- package/packages/daemon/src/sessions/policyTypes.ts +94 -0
- package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
- package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
- package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
- package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
- package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
- package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
- package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
- package/packages/daemon/src/workflows/contextManager.ts +83 -0
- package/packages/daemon/src/workflows/index.ts +31 -0
- package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
- package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
- package/packages/daemon/src/workflows/workflowParser.ts +217 -0
- package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
- package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
- package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
- package/packages/providers/claude-code/package.json +11 -0
- package/packages/providers/claude-code/src/index.ts +7 -0
- package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
- package/packages/providers/claude-code/src/provider.ts +311 -0
- package/packages/web/dist/android-chrome-192x192.png +0 -0
- package/packages/web/dist/android-chrome-512x512.png +0 -0
- package/packages/web/dist/apple-touch-icon.png +0 -0
- package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
- package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
- package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
- package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
- package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
- package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
- package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
- package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
- package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
- package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
- package/packages/web/dist/assets/index-hgphORiw.js +204 -0
- package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
- package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
- package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
- package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
- package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
- package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
- package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
- package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
- package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
- package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
- package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
- package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
- package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/icon.svg +1 -0
- package/packages/web/dist/index.html +29 -0
- package/packages/web/dist/manifest.json +29 -0
- package/packages/web/dist/og-image.png +0 -0
- package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
- package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
- package/packages/web/dist/originals/apple-touch-icon.png +0 -0
- package/packages/web/dist/originals/favicon.ico +0 -0
- package/packages/web/dist/piper.svg +1 -0
- package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
- package/packages/web/dist/sw.js +257 -0
- package/scripts/postinstall-link-workspaces.mjs +58 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolicyEngine - Evaluates PermissionRequest events against policy rules
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Pattern matching (glob, negation, arrays)
|
|
6
|
+
* - Rule precedence (priority + order)
|
|
7
|
+
* - Context matching (tool, args, cwd, session)
|
|
8
|
+
* - Default policy fallback
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { matchPattern, valueToString } from "./policyMatcher";
|
|
12
|
+
import type { PermissionRequest, Policy, PolicyDecision, PolicyRule } from "./policyTypes";
|
|
13
|
+
|
|
14
|
+
export type DefaultPolicyAction = "ask" | "deny";
|
|
15
|
+
|
|
16
|
+
export interface PolicyEngineOptions {
|
|
17
|
+
/** Default action when no rules match (only "ask" or "deny" — "allow" requires explicit rules) */
|
|
18
|
+
defaultAction?: DefaultPolicyAction;
|
|
19
|
+
|
|
20
|
+
/** Default reason when no rules match */
|
|
21
|
+
defaultReason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PolicyEngine {
|
|
25
|
+
private defaultAction: DefaultPolicyAction;
|
|
26
|
+
private defaultReason: string;
|
|
27
|
+
|
|
28
|
+
constructor(options: PolicyEngineOptions = {}) {
|
|
29
|
+
this.defaultAction = options.defaultAction ?? "ask";
|
|
30
|
+
this.defaultReason = options.defaultReason ?? "No matching policy rule found (default ask)";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Update the default action at runtime (e.g., when daemon settings change)
|
|
35
|
+
*/
|
|
36
|
+
setDefaultAction(action: DefaultPolicyAction): void {
|
|
37
|
+
this.defaultAction = action;
|
|
38
|
+
this.defaultReason =
|
|
39
|
+
action === "ask"
|
|
40
|
+
? "No matching policy rule found (default ask)"
|
|
41
|
+
: `No matching policy rule found (default ${action})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the current default action
|
|
46
|
+
*/
|
|
47
|
+
getDefaultAction(): DefaultPolicyAction {
|
|
48
|
+
return this.defaultAction;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate a permission request against a list of policies
|
|
53
|
+
*
|
|
54
|
+
* Algorithm:
|
|
55
|
+
* 1. Filter policies by session ID and enabled status
|
|
56
|
+
* 2. Sort by priority (higher = more specific, evaluated first)
|
|
57
|
+
* 3. Evaluate each rule in priority order
|
|
58
|
+
* 4. First match wins
|
|
59
|
+
* 5. If no match, return default policy
|
|
60
|
+
*
|
|
61
|
+
* @param request The permission request to evaluate
|
|
62
|
+
* @param policies All available policies
|
|
63
|
+
* @returns The policy decision
|
|
64
|
+
*/
|
|
65
|
+
async evaluate(request: PermissionRequest, policies: Policy[]): Promise<PolicyDecision> {
|
|
66
|
+
// Filter and sort policies
|
|
67
|
+
const applicablePolicies = policies
|
|
68
|
+
.filter((p) => this.isPolicyApplicable(p, request))
|
|
69
|
+
.sort((a, b) => b.priority - a.priority); // Higher priority first
|
|
70
|
+
|
|
71
|
+
// Evaluate each policy's rules in order
|
|
72
|
+
for (const policy of applicablePolicies) {
|
|
73
|
+
for (const rule of policy.rules) {
|
|
74
|
+
if (this.ruleMatches(rule, request)) {
|
|
75
|
+
// First match wins
|
|
76
|
+
return {
|
|
77
|
+
action: rule.action,
|
|
78
|
+
reason: rule.reason,
|
|
79
|
+
policyId: policy.id,
|
|
80
|
+
ruleId: rule.id,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// No match - return default policy
|
|
87
|
+
return this.getDefaultPolicy();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a policy is applicable to a request
|
|
92
|
+
*/
|
|
93
|
+
private isPolicyApplicable(policy: Policy, request: PermissionRequest): boolean {
|
|
94
|
+
// Only enabled policies
|
|
95
|
+
if (!policy.enabled) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check session ID match
|
|
100
|
+
if (policy.sessionId !== undefined) {
|
|
101
|
+
if (policy.sessionId !== request.sessionId) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a rule matches a permission request
|
|
111
|
+
*
|
|
112
|
+
* All criteria must match for the rule to match:
|
|
113
|
+
* - Tool name (if specified)
|
|
114
|
+
* - Arguments (if specified, all must match)
|
|
115
|
+
* - CWD (if specified)
|
|
116
|
+
* - Session (if specified)
|
|
117
|
+
*/
|
|
118
|
+
private ruleMatches(rule: PolicyRule, request: PermissionRequest): boolean {
|
|
119
|
+
// Match tool name
|
|
120
|
+
if (rule.tool !== undefined) {
|
|
121
|
+
if (!matchPattern(request.tool, rule.tool)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Match arguments
|
|
127
|
+
if (rule.args !== undefined) {
|
|
128
|
+
for (const [key, pattern] of Object.entries(rule.args)) {
|
|
129
|
+
const value = request.args[key];
|
|
130
|
+
const valueStr = valueToString(value);
|
|
131
|
+
|
|
132
|
+
if (!(valueStr && matchPattern(valueStr, pattern))) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Match CWD
|
|
139
|
+
if (rule.cwd !== undefined) {
|
|
140
|
+
if (!matchPattern(request.cwd, rule.cwd)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Match session
|
|
146
|
+
if (rule.session !== undefined) {
|
|
147
|
+
if (!matchPattern(request.sessionId, rule.session)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// All criteria matched
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the default policy when no rules match
|
|
158
|
+
*/
|
|
159
|
+
private getDefaultPolicy(): PolicyDecision {
|
|
160
|
+
return {
|
|
161
|
+
action: this.defaultAction,
|
|
162
|
+
reason: this.defaultReason,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolicyMatcher - Pattern matching utilities for policy rules
|
|
3
|
+
* Supports glob patterns, negation, and arrays
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import micromatch from "micromatch";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Match a value against a pattern or array of patterns
|
|
10
|
+
*
|
|
11
|
+
* Supports:
|
|
12
|
+
* - Exact matching: "Read" matches "Read"
|
|
13
|
+
* - Glob patterns: "Read*" matches "ReadFile"
|
|
14
|
+
* - Wildcards: "*Read*" matches "FileRead"
|
|
15
|
+
* - Negation: "!*.env" matches anything except *.env
|
|
16
|
+
* - Arrays: ["Read", "Write"] matches either
|
|
17
|
+
*
|
|
18
|
+
* @param value The value to match
|
|
19
|
+
* @param pattern The pattern(s) to match against
|
|
20
|
+
* @returns true if the value matches the pattern
|
|
21
|
+
*/
|
|
22
|
+
export function matchPattern(value: string, pattern: string | string[]): boolean {
|
|
23
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern];
|
|
24
|
+
|
|
25
|
+
// Track if we have any negative patterns
|
|
26
|
+
const negativePatterns: string[] = [];
|
|
27
|
+
const positivePatterns: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const p of patterns) {
|
|
30
|
+
// Skip empty patterns
|
|
31
|
+
if (p === "") {
|
|
32
|
+
// Empty pattern matches empty value
|
|
33
|
+
if (value === "") {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (p.startsWith("!")) {
|
|
40
|
+
const negPattern = p.slice(1);
|
|
41
|
+
if (negPattern !== "") {
|
|
42
|
+
negativePatterns.push(negPattern);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
positivePatterns.push(p);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Micromatch options for better pattern matching
|
|
50
|
+
// bash: true treats patterns as bash globs (not file paths)
|
|
51
|
+
const options = { bash: true };
|
|
52
|
+
|
|
53
|
+
// If value matches any negative pattern, fail immediately
|
|
54
|
+
if (negativePatterns.length > 0) {
|
|
55
|
+
if (micromatch.isMatch(value, negativePatterns, options)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If we have positive patterns, check if any match
|
|
61
|
+
if (positivePatterns.length > 0) {
|
|
62
|
+
return micromatch.isMatch(value, positivePatterns, options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If we only had negative patterns and they didn't match, still fail
|
|
66
|
+
// because we have no positive pattern to indicate what SHOULD match
|
|
67
|
+
// Negative patterns alone mean "deny these" but don't specify what to allow
|
|
68
|
+
if (negativePatterns.length > 0) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// No patterns at all
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalize a path for consistent matching
|
|
78
|
+
* Removes trailing slashes and resolves relative paths
|
|
79
|
+
*/
|
|
80
|
+
export function normalizePath(path: string): string {
|
|
81
|
+
// Remove trailing slash
|
|
82
|
+
let normalized = path.replace(/\/+$/, "");
|
|
83
|
+
|
|
84
|
+
// If empty after removing slashes, it was root
|
|
85
|
+
if (normalized === "") {
|
|
86
|
+
normalized = "/";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert a value to string for pattern matching
|
|
94
|
+
* Handles non-string values gracefully
|
|
95
|
+
*/
|
|
96
|
+
export function valueToString(value: unknown): string {
|
|
97
|
+
if (value === null || value === undefined) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof value === "string") {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
106
|
+
return String(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof value === "object") {
|
|
110
|
+
return JSON.stringify(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return String(value);
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolicyTypes - Core types for the policy engine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Policy rule action types
|
|
7
|
+
*/
|
|
8
|
+
export type PolicyAction = "allow" | "deny" | "ask";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A single policy rule that matches against PermissionRequest criteria
|
|
12
|
+
*/
|
|
13
|
+
export interface PolicyRule {
|
|
14
|
+
/** Unique identifier for this rule */
|
|
15
|
+
id: string;
|
|
16
|
+
|
|
17
|
+
/** Action to take if this rule matches */
|
|
18
|
+
action: PolicyAction;
|
|
19
|
+
|
|
20
|
+
/** Tool name pattern(s) to match against (glob supported) */
|
|
21
|
+
tool?: string | string[];
|
|
22
|
+
|
|
23
|
+
/** Argument patterns to match (key -> pattern) */
|
|
24
|
+
args?: Record<string, string | string[]>;
|
|
25
|
+
|
|
26
|
+
/** CWD pattern(s) to match against */
|
|
27
|
+
cwd?: string | string[];
|
|
28
|
+
|
|
29
|
+
/** Session ID pattern(s) to match against */
|
|
30
|
+
session?: string | string[];
|
|
31
|
+
|
|
32
|
+
/** Human-readable explanation for the decision */
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A policy contains multiple rules and metadata
|
|
38
|
+
*/
|
|
39
|
+
export interface Policy {
|
|
40
|
+
/** Unique identifier */
|
|
41
|
+
id: string;
|
|
42
|
+
|
|
43
|
+
/** Human-readable name */
|
|
44
|
+
name: string;
|
|
45
|
+
|
|
46
|
+
/** Optional description */
|
|
47
|
+
description?: string;
|
|
48
|
+
|
|
49
|
+
/** Whether this policy is active */
|
|
50
|
+
enabled: boolean;
|
|
51
|
+
|
|
52
|
+
/** Higher priority = evaluated first (more specific) */
|
|
53
|
+
priority: number;
|
|
54
|
+
|
|
55
|
+
/** Session ID this policy applies to (null = global) */
|
|
56
|
+
sessionId?: string;
|
|
57
|
+
|
|
58
|
+
/** Rules to evaluate in order */
|
|
59
|
+
rules: PolicyRule[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The result of evaluating a permission request against policies
|
|
64
|
+
*/
|
|
65
|
+
export interface PolicyDecision {
|
|
66
|
+
/** The action to take */
|
|
67
|
+
action: PolicyAction;
|
|
68
|
+
|
|
69
|
+
/** Explanation for the decision */
|
|
70
|
+
reason?: string;
|
|
71
|
+
|
|
72
|
+
/** ID of the policy that matched */
|
|
73
|
+
policyId?: string;
|
|
74
|
+
|
|
75
|
+
/** ID of the specific rule that matched */
|
|
76
|
+
ruleId?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A permission request from Claude Code hooks
|
|
81
|
+
*/
|
|
82
|
+
export interface PermissionRequest {
|
|
83
|
+
/** Session ID making the request */
|
|
84
|
+
sessionId: string;
|
|
85
|
+
|
|
86
|
+
/** Tool name being invoked */
|
|
87
|
+
tool: string;
|
|
88
|
+
|
|
89
|
+
/** Tool arguments */
|
|
90
|
+
args: Record<string, unknown>;
|
|
91
|
+
|
|
92
|
+
/** Current working directory */
|
|
93
|
+
cwd: string;
|
|
94
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY (Pseudo-Terminal) process wrapper for Bun
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Subprocess } from "bun";
|
|
6
|
+
|
|
7
|
+
export interface PTYProcessOptions {
|
|
8
|
+
command: string[];
|
|
9
|
+
cwd: string;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
cols?: number;
|
|
12
|
+
rows?: number;
|
|
13
|
+
onData?: (data: string) => void;
|
|
14
|
+
onExit?: (exitCode: number, signal: string | null) => void;
|
|
15
|
+
onDrain?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PTYProcess {
|
|
19
|
+
private proc: Subprocess;
|
|
20
|
+
public pid: number;
|
|
21
|
+
public cols: number;
|
|
22
|
+
public rows: number;
|
|
23
|
+
public closed: boolean = false;
|
|
24
|
+
|
|
25
|
+
constructor(options: PTYProcessOptions) {
|
|
26
|
+
const { command, cwd, env, cols = 80, rows = 24, onData, onExit, onDrain } = options;
|
|
27
|
+
|
|
28
|
+
this.cols = cols;
|
|
29
|
+
this.rows = rows;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
this.proc = Bun.spawn(command, {
|
|
33
|
+
cwd,
|
|
34
|
+
env: env || process.env,
|
|
35
|
+
terminal: {
|
|
36
|
+
cols,
|
|
37
|
+
rows,
|
|
38
|
+
data: (_terminal, data) => {
|
|
39
|
+
if (onData) {
|
|
40
|
+
const text = new TextDecoder().decode(data);
|
|
41
|
+
onData(text);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
exit: (_terminal, exitCode, signal) => {
|
|
45
|
+
this.closed = true;
|
|
46
|
+
if (onExit) {
|
|
47
|
+
onExit(exitCode, signal);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
drain: (_terminal) => {
|
|
51
|
+
if (onDrain) {
|
|
52
|
+
onDrain();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!this.proc.terminal) {
|
|
59
|
+
throw new Error("Failed to create PTY - terminal object is undefined");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.pid = this.proc.pid;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Failed to spawn process: ${error}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
write(data: string): void {
|
|
69
|
+
if (this.closed) {
|
|
70
|
+
throw new Error("Cannot write to closed PTY");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!this.proc.terminal) {
|
|
74
|
+
throw new Error("PTY terminal is not available");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.proc.terminal.write(data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
resize(cols: number, rows: number): void {
|
|
81
|
+
if (this.closed) {
|
|
82
|
+
throw new Error("Cannot resize closed PTY");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!this.proc.terminal) {
|
|
86
|
+
throw new Error("PTY terminal is not available");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.cols = cols;
|
|
90
|
+
this.rows = rows;
|
|
91
|
+
this.proc.terminal.resize(cols, rows);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setRawMode(enabled: boolean): void {
|
|
95
|
+
if (this.closed) {
|
|
96
|
+
throw new Error("Cannot set raw mode on closed PTY");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!this.proc.terminal) {
|
|
100
|
+
throw new Error("PTY terminal is not available");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.proc.terminal.setRawMode(enabled);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async kill(signal?: string): Promise<void> {
|
|
107
|
+
if (this.closed) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// Close terminal first to trigger clean shutdown
|
|
113
|
+
if (this.proc.terminal && !this.proc.terminal.closed) {
|
|
114
|
+
this.proc.terminal.close();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Then kill the process
|
|
118
|
+
this.proc.kill(signal as any);
|
|
119
|
+
|
|
120
|
+
// Wait for process to exit with timeout
|
|
121
|
+
await Promise.race([this.proc.exited, new Promise((resolve) => setTimeout(resolve, 1000))]);
|
|
122
|
+
|
|
123
|
+
this.closed = true;
|
|
124
|
+
} catch (_error) {
|
|
125
|
+
// Process might already be dead
|
|
126
|
+
this.closed = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ref(): void {
|
|
131
|
+
if (this.proc.terminal) {
|
|
132
|
+
this.proc.terminal.ref();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
unref(): void {
|
|
137
|
+
if (this.proc.terminal) {
|
|
138
|
+
this.proc.terminal.unref();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|