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,369 @@
|
|
|
1
|
+
import type { EventBus } from "@codepiper/core";
|
|
2
|
+
import webpush from "web-push";
|
|
3
|
+
import type { IDatabase, PushSubscriptionRecord } from "../db/db";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PUSH_SUBJECT = "mailto:push@codepiper.dev";
|
|
6
|
+
const PUSH_ENABLED_ENV = "CODEPIPER_PUSH_ENABLED";
|
|
7
|
+
const PUSH_PUBLIC_KEY_ENV = "CODEPIPER_PUSH_PUBLIC_KEY";
|
|
8
|
+
const PUSH_PRIVATE_KEY_ENV = "CODEPIPER_PUSH_PRIVATE_KEY";
|
|
9
|
+
const PUSH_SUBJECT_ENV = "CODEPIPER_PUSH_SUBJECT";
|
|
10
|
+
|
|
11
|
+
type ConsoleLike = Pick<Console, "warn" | "error">;
|
|
12
|
+
|
|
13
|
+
export interface NotificationCreatedEvent {
|
|
14
|
+
id: number;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
eventType: string;
|
|
17
|
+
title: string;
|
|
18
|
+
body: string | null;
|
|
19
|
+
payload?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PushRuntimeStatus {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
configured: boolean;
|
|
25
|
+
reasons: string[];
|
|
26
|
+
publicKey: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PushDeliveryResult {
|
|
30
|
+
attempted: number;
|
|
31
|
+
delivered: number;
|
|
32
|
+
expired: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
skipped: boolean;
|
|
35
|
+
reason?: "not_available" | "no_subscriptions";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface WebPushClient {
|
|
39
|
+
setVapidDetails(subject: string, publicKey: string, privateKey: string): void;
|
|
40
|
+
sendNotification(
|
|
41
|
+
subscription: {
|
|
42
|
+
endpoint: string;
|
|
43
|
+
expirationTime: number | null;
|
|
44
|
+
keys: { p256dh: string; auth: string };
|
|
45
|
+
},
|
|
46
|
+
payload?: string
|
|
47
|
+
): Promise<unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type PushNotifierDatabase = Pick<IDatabase, "listPushSubscriptions" | "deletePushSubscription">;
|
|
51
|
+
|
|
52
|
+
export interface PushNotifierOptions {
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
vapidPublicKey?: string | null;
|
|
55
|
+
vapidPrivateKey?: string | null;
|
|
56
|
+
vapidSubject?: string | null;
|
|
57
|
+
webPushClient?: WebPushClient;
|
|
58
|
+
logger?: ConsoleLike;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toTrimmedEnv(value: string | null | undefined): string {
|
|
62
|
+
return typeof value === "string" ? value.trim() : "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isNotificationCreatedEvent(value: unknown): value is NotificationCreatedEvent {
|
|
70
|
+
if (!isRecord(value)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const payload = value.payload;
|
|
75
|
+
const payloadValid = payload === undefined || isRecord(payload);
|
|
76
|
+
return (
|
|
77
|
+
typeof value.id === "number" &&
|
|
78
|
+
typeof value.sessionId === "string" &&
|
|
79
|
+
typeof value.eventType === "string" &&
|
|
80
|
+
typeof value.title === "string" &&
|
|
81
|
+
(typeof value.body === "string" || value.body === null) &&
|
|
82
|
+
payloadValid
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isExpiredSubscriptionError(error: unknown): boolean {
|
|
87
|
+
if (!isRecord(error)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const statusCode = error.statusCode;
|
|
91
|
+
return statusCode === 404 || statusCode === 410;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toWebPushSubscription(subscription: PushSubscriptionRecord): {
|
|
95
|
+
endpoint: string;
|
|
96
|
+
expirationTime: number | null;
|
|
97
|
+
keys: { p256dh: string; auth: string };
|
|
98
|
+
} {
|
|
99
|
+
return {
|
|
100
|
+
endpoint: subscription.endpoint,
|
|
101
|
+
expirationTime: subscription.expirationTime ?? null,
|
|
102
|
+
keys: {
|
|
103
|
+
p256dh: subscription.keys.p256dh,
|
|
104
|
+
auth: subscription.keys.auth,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveSessionLabel(event: NotificationCreatedEvent): string {
|
|
110
|
+
if (isRecord(event.payload) && typeof event.payload.sessionLabel === "string") {
|
|
111
|
+
const candidate = event.payload.sessionLabel.trim();
|
|
112
|
+
if (candidate.length > 0) {
|
|
113
|
+
return candidate;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `session ${event.sessionId.slice(0, 8)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function bodyIncludesSessionLabel(body: string, sessionLabel: string): boolean {
|
|
121
|
+
return body.toLocaleLowerCase().includes(sessionLabel.toLocaleLowerCase());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureBodyIncludesSessionLabel(body: string | null, sessionLabel: string): string | null {
|
|
125
|
+
if (!(typeof body === "string" && body.trim().length > 0)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const normalized = body.trim();
|
|
130
|
+
if (bodyIncludesSessionLabel(normalized, sessionLabel)) {
|
|
131
|
+
return normalized;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `${sessionLabel}: ${normalized}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ensureTitleIncludesSessionLabel(title: string, sessionLabel: string): string {
|
|
138
|
+
if (bodyIncludesSessionLabel(title, sessionLabel)) {
|
|
139
|
+
return title;
|
|
140
|
+
}
|
|
141
|
+
return `${sessionLabel} · ${title}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function toNotificationPayload(event: NotificationCreatedEvent): string {
|
|
145
|
+
const sessionLabel = resolveSessionLabel(event);
|
|
146
|
+
const fallbackBody =
|
|
147
|
+
event.eventType === "session.turn_completed"
|
|
148
|
+
? `${sessionLabel} is ready for your next prompt.`
|
|
149
|
+
: event.eventType === "session.permission_required"
|
|
150
|
+
? `${sessionLabel} is waiting for your permission approval.`
|
|
151
|
+
: event.eventType === "session.input_required"
|
|
152
|
+
? `${sessionLabel} is waiting for your input.`
|
|
153
|
+
: `${sessionLabel} has an update: ${event.eventType.replaceAll(/[._]+/g, " ")}`;
|
|
154
|
+
const body = ensureBodyIncludesSessionLabel(event.body, sessionLabel) ?? fallbackBody;
|
|
155
|
+
const title = ensureTitleIncludesSessionLabel(event.title, sessionLabel);
|
|
156
|
+
const payload = {
|
|
157
|
+
title,
|
|
158
|
+
body,
|
|
159
|
+
sessionId: event.sessionId,
|
|
160
|
+
sessionLabel,
|
|
161
|
+
notificationId: event.id,
|
|
162
|
+
url: `/sessions/${encodeURIComponent(event.sessionId)}/terminal`,
|
|
163
|
+
tag: `codepiper:notification:${event.id}`,
|
|
164
|
+
};
|
|
165
|
+
return JSON.stringify(payload);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class PushNotifier {
|
|
169
|
+
private readonly db: PushNotifierDatabase;
|
|
170
|
+
private readonly eventBus: EventBus<Record<string, unknown>>;
|
|
171
|
+
private readonly logger: ConsoleLike;
|
|
172
|
+
private readonly webPushClient: WebPushClient;
|
|
173
|
+
private readonly enabled: boolean;
|
|
174
|
+
private readonly configured: boolean;
|
|
175
|
+
private readonly baseReasons: string[];
|
|
176
|
+
private readonly publicKey: string | null;
|
|
177
|
+
private unsubscribeNotificationCreated: (() => void) | null = null;
|
|
178
|
+
private pendingEvents: NotificationCreatedEvent[] = [];
|
|
179
|
+
private queueProcessing = false;
|
|
180
|
+
|
|
181
|
+
constructor(
|
|
182
|
+
db: PushNotifierDatabase,
|
|
183
|
+
eventBus: EventBus<Record<string, unknown>>,
|
|
184
|
+
options: PushNotifierOptions = {}
|
|
185
|
+
) {
|
|
186
|
+
this.db = db;
|
|
187
|
+
this.eventBus = eventBus;
|
|
188
|
+
this.logger = options.logger ?? console;
|
|
189
|
+
this.webPushClient = options.webPushClient ?? webpush;
|
|
190
|
+
this.enabled = options.enabled ?? process.env[PUSH_ENABLED_ENV] === "1";
|
|
191
|
+
this.baseReasons = [];
|
|
192
|
+
this.publicKey = null;
|
|
193
|
+
|
|
194
|
+
if (!this.enabled) {
|
|
195
|
+
this.baseReasons.push("feature_disabled");
|
|
196
|
+
this.configured = false;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const vapidPublicKey = toTrimmedEnv(
|
|
201
|
+
options.vapidPublicKey ?? process.env[PUSH_PUBLIC_KEY_ENV] ?? null
|
|
202
|
+
);
|
|
203
|
+
const vapidPrivateKey = toTrimmedEnv(
|
|
204
|
+
options.vapidPrivateKey ?? process.env[PUSH_PRIVATE_KEY_ENV] ?? null
|
|
205
|
+
);
|
|
206
|
+
const vapidSubject = toTrimmedEnv(
|
|
207
|
+
options.vapidSubject ?? process.env[PUSH_SUBJECT_ENV] ?? DEFAULT_PUSH_SUBJECT
|
|
208
|
+
);
|
|
209
|
+
this.publicKey = vapidPublicKey || null;
|
|
210
|
+
|
|
211
|
+
if (!vapidPublicKey) {
|
|
212
|
+
this.baseReasons.push("missing_vapid_public_key");
|
|
213
|
+
}
|
|
214
|
+
if (!vapidPrivateKey) {
|
|
215
|
+
this.baseReasons.push("missing_vapid_private_key");
|
|
216
|
+
}
|
|
217
|
+
if (!(vapidPublicKey && vapidPrivateKey)) {
|
|
218
|
+
this.logger.warn(
|
|
219
|
+
`[push] ${PUSH_ENABLED_ENV}=1 but VAPID keys are missing; push delivery disabled`
|
|
220
|
+
);
|
|
221
|
+
this.configured = false;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
this.webPushClient.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey);
|
|
227
|
+
this.configured = true;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this.logger.error("[push] Failed to configure VAPID details; push delivery disabled", error);
|
|
230
|
+
this.baseReasons.push("invalid_vapid_configuration");
|
|
231
|
+
this.configured = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getStatus(): PushRuntimeStatus {
|
|
236
|
+
return {
|
|
237
|
+
enabled: this.enabled,
|
|
238
|
+
configured: this.configured,
|
|
239
|
+
reasons: Array.from(new Set(this.baseReasons)),
|
|
240
|
+
publicKey: this.publicKey,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
start(): void {
|
|
245
|
+
if (!(this.enabled && this.configured) || this.unsubscribeNotificationCreated) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.unsubscribeNotificationCreated = this.eventBus.on("notification:created", (event) => {
|
|
250
|
+
if (!isNotificationCreatedEvent(event)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.pendingEvents.push(event);
|
|
254
|
+
void this.drainQueue();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
stop(): void {
|
|
259
|
+
if (this.unsubscribeNotificationCreated) {
|
|
260
|
+
this.unsubscribeNotificationCreated();
|
|
261
|
+
this.unsubscribeNotificationCreated = null;
|
|
262
|
+
}
|
|
263
|
+
this.pendingEvents = [];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async sendTestNotification(params?: {
|
|
267
|
+
title?: string;
|
|
268
|
+
body?: string;
|
|
269
|
+
sessionId?: string;
|
|
270
|
+
}): Promise<PushDeliveryResult> {
|
|
271
|
+
if (!(this.enabled && this.configured)) {
|
|
272
|
+
return {
|
|
273
|
+
attempted: 0,
|
|
274
|
+
delivered: 0,
|
|
275
|
+
expired: 0,
|
|
276
|
+
failed: 0,
|
|
277
|
+
skipped: true,
|
|
278
|
+
reason: "not_available",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const subscriptions = this.db.listPushSubscriptions();
|
|
283
|
+
if (subscriptions.length === 0) {
|
|
284
|
+
return {
|
|
285
|
+
attempted: 0,
|
|
286
|
+
delivered: 0,
|
|
287
|
+
expired: 0,
|
|
288
|
+
failed: 0,
|
|
289
|
+
skipped: true,
|
|
290
|
+
reason: "no_subscriptions",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const event: NotificationCreatedEvent = {
|
|
295
|
+
id: Date.now(),
|
|
296
|
+
sessionId: params?.sessionId?.trim() || "push-test",
|
|
297
|
+
eventType: "session.turn_completed",
|
|
298
|
+
title: params?.title?.trim() || "CodePiper test notification",
|
|
299
|
+
body: params?.body?.trim() || "Push delivery is working.",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const payload = toNotificationPayload(event);
|
|
303
|
+
return this.deliverPayloadToSubscriptions(payload, subscriptions);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async drainQueue(): Promise<void> {
|
|
307
|
+
if (this.queueProcessing) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.queueProcessing = true;
|
|
312
|
+
try {
|
|
313
|
+
while (this.pendingEvents.length > 0) {
|
|
314
|
+
const event = this.pendingEvents.shift();
|
|
315
|
+
if (!event) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
await this.deliverEvent(event);
|
|
319
|
+
}
|
|
320
|
+
} finally {
|
|
321
|
+
this.queueProcessing = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async deliverEvent(event: NotificationCreatedEvent): Promise<void> {
|
|
326
|
+
const subscriptions = this.db.listPushSubscriptions();
|
|
327
|
+
if (subscriptions.length === 0) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const payload = toNotificationPayload(event);
|
|
332
|
+
await this.deliverPayloadToSubscriptions(payload, subscriptions);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async deliverPayloadToSubscriptions(
|
|
336
|
+
payload: string,
|
|
337
|
+
subscriptions: PushSubscriptionRecord[]
|
|
338
|
+
): Promise<PushDeliveryResult> {
|
|
339
|
+
const result: PushDeliveryResult = {
|
|
340
|
+
attempted: subscriptions.length,
|
|
341
|
+
delivered: 0,
|
|
342
|
+
expired: 0,
|
|
343
|
+
failed: 0,
|
|
344
|
+
skipped: false,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
await Promise.all(
|
|
348
|
+
subscriptions.map(async (subscription) => {
|
|
349
|
+
try {
|
|
350
|
+
await this.webPushClient.sendNotification(toWebPushSubscription(subscription), payload);
|
|
351
|
+
result.delivered += 1;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (isExpiredSubscriptionError(error)) {
|
|
354
|
+
this.db.deletePushSubscription(subscription.endpoint);
|
|
355
|
+
result.expired += 1;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
result.failed += 1;
|
|
360
|
+
const statusCode =
|
|
361
|
+
isRecord(error) && typeof error.statusCode === "number" ? error.statusCode : "unknown";
|
|
362
|
+
this.logger.warn(`[push] Delivery failed for a subscription (status=${statusCode})`);
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ProviderBuildCommandArgs } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface CodexAppServerSpikeState {
|
|
4
|
+
configured: boolean;
|
|
5
|
+
enrolled: boolean;
|
|
6
|
+
mode: "tmux-cli-fallback";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CODEX_HOST_ACCESS_RUNTIME_ARGS = [
|
|
10
|
+
"--sandbox",
|
|
11
|
+
"danger-full-access",
|
|
12
|
+
"-a",
|
|
13
|
+
"on-request",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export function resolveCodexAppServerSpikeState(
|
|
17
|
+
args: Pick<ProviderBuildCommandArgs, "terminalFeatures">
|
|
18
|
+
): CodexAppServerSpikeState {
|
|
19
|
+
const configured = args.terminalFeatures?.codexAppServerSpikeEnabled === true;
|
|
20
|
+
const enrolled = configured;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
configured,
|
|
24
|
+
enrolled,
|
|
25
|
+
// Scaffold phase: enrollment metadata exists, runtime remains tmux CLI.
|
|
26
|
+
// Intentionally boolean-only (no canary): CodePiper targets single-user deployments.
|
|
27
|
+
mode: "tmux-cli-fallback",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildCodexCommand(args: ProviderBuildCommandArgs): string[] {
|
|
32
|
+
const command = ["codex"];
|
|
33
|
+
if (args.dangerousMode) {
|
|
34
|
+
command.push("--dangerously-bypass-approvals-and-sandbox");
|
|
35
|
+
} else if (args.codexHostAccessProfileEnabled) {
|
|
36
|
+
command.push(...CODEX_HOST_ACCESS_RUNTIME_ARGS);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (args.providerResume) {
|
|
40
|
+
const mode = args.providerResume.mode === "fork" ? "fork" : "resume";
|
|
41
|
+
command.push(mode);
|
|
42
|
+
command.push(...args.providerArgs);
|
|
43
|
+
command.push(args.providerResume.providerSessionId);
|
|
44
|
+
return command;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
command.push(...args.providerArgs);
|
|
48
|
+
return command;
|
|
49
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type KnownProviderId, type ProviderId, SUPPORTED_PROVIDERS } from "@codepiper/core";
|
|
2
|
+
import { generateOverlaySettings } from "@codepiper/provider-claude-code";
|
|
3
|
+
import { buildCodexCommand } from "./codexAppServerScaffold";
|
|
4
|
+
import type { ProviderDefinition } from "./types";
|
|
5
|
+
|
|
6
|
+
const PROVIDERS: Record<KnownProviderId, ProviderDefinition> = {
|
|
7
|
+
"claude-code": {
|
|
8
|
+
id: "claude-code",
|
|
9
|
+
label: "Claude Code",
|
|
10
|
+
runtime: "tmux",
|
|
11
|
+
capabilities: {
|
|
12
|
+
nativeHooks: true,
|
|
13
|
+
supportsDangerousMode: true,
|
|
14
|
+
supportsModelSwitch: true,
|
|
15
|
+
supportsTranscriptTailing: true,
|
|
16
|
+
supportsTmuxAdoption: true,
|
|
17
|
+
policyChannel: "native-hooks",
|
|
18
|
+
metricsChannel: "transcript",
|
|
19
|
+
},
|
|
20
|
+
launchHints: {
|
|
21
|
+
dangerousModeFlags: ["--dangerously-skip-permissions"],
|
|
22
|
+
resumeCommands: {
|
|
23
|
+
resume: "claude --resume {id}",
|
|
24
|
+
fork: "claude --resume {id} --fork-session",
|
|
25
|
+
idPlaceholder: "claude session id",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
async prepareSession(args) {
|
|
29
|
+
const settingsPath = await generateOverlaySettings({
|
|
30
|
+
sessionId: args.sessionId,
|
|
31
|
+
socketPath: args.socketPath,
|
|
32
|
+
secret: args.secret,
|
|
33
|
+
outputDir: args.runtimeDir,
|
|
34
|
+
enableStatusline: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
settingsPath,
|
|
39
|
+
cleanupArtifacts: [
|
|
40
|
+
`${args.sessionId}.json`,
|
|
41
|
+
`${args.sessionId}.hook-forward.sh`,
|
|
42
|
+
`${args.sessionId}.statusline-forward.sh`,
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
buildCommand(args) {
|
|
47
|
+
const command = ["claude"];
|
|
48
|
+
if (args.providerResume) {
|
|
49
|
+
command.push("--resume", args.providerResume.providerSessionId);
|
|
50
|
+
if (args.providerResume.mode === "fork") {
|
|
51
|
+
command.push("--fork-session");
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
command.push("--session-id", args.sessionId);
|
|
55
|
+
}
|
|
56
|
+
if (args.dangerousMode) {
|
|
57
|
+
command.push("--dangerously-skip-permissions");
|
|
58
|
+
}
|
|
59
|
+
if (args.settingsPath) {
|
|
60
|
+
command.push("--settings", args.settingsPath);
|
|
61
|
+
}
|
|
62
|
+
command.push(...args.providerArgs);
|
|
63
|
+
return command;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
codex: {
|
|
67
|
+
id: "codex",
|
|
68
|
+
label: "Codex",
|
|
69
|
+
runtime: "tmux",
|
|
70
|
+
capabilities: {
|
|
71
|
+
nativeHooks: false,
|
|
72
|
+
supportsDangerousMode: true,
|
|
73
|
+
supportsModelSwitch: false,
|
|
74
|
+
supportsTranscriptTailing: false,
|
|
75
|
+
supportsTmuxAdoption: true,
|
|
76
|
+
policyChannel: "input-preflight",
|
|
77
|
+
metricsChannel: "pty",
|
|
78
|
+
},
|
|
79
|
+
launchHints: {
|
|
80
|
+
dangerousModeFlags: ["--dangerously-bypass-approvals-and-sandbox"],
|
|
81
|
+
resumeCommands: {
|
|
82
|
+
resume: "codex resume {id}",
|
|
83
|
+
fork: "codex fork {id}",
|
|
84
|
+
idPlaceholder: "019c7285-ba64-7462-bbfc-4227f3e24e88",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
buildCommand(args) {
|
|
88
|
+
return buildCodexCommand(args);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function listSupportedProviders(): KnownProviderId[] {
|
|
94
|
+
return [...SUPPORTED_PROVIDERS];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isProviderId(value: string): value is KnownProviderId {
|
|
98
|
+
return listSupportedProviders().includes(value as KnownProviderId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getProviderDefinition(providerId: ProviderId): ProviderDefinition {
|
|
102
|
+
const definition = PROVIDERS[providerId as KnownProviderId];
|
|
103
|
+
if (!definition) {
|
|
104
|
+
throw new Error(`Unsupported provider: ${providerId}`);
|
|
105
|
+
}
|
|
106
|
+
return definition;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function listProviderDefinitions(): ProviderDefinition[] {
|
|
110
|
+
return listSupportedProviders().map((providerId) => getProviderDefinition(providerId));
|
|
111
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ProviderId } from "@codepiper/core";
|
|
2
|
+
|
|
3
|
+
export type ProviderPolicyChannel = "native-hooks" | "input-preflight" | "none";
|
|
4
|
+
export type ProviderMetricsChannel = "transcript" | "pty" | "none";
|
|
5
|
+
|
|
6
|
+
export interface ProviderCapabilities {
|
|
7
|
+
nativeHooks: boolean;
|
|
8
|
+
supportsDangerousMode: boolean;
|
|
9
|
+
supportsModelSwitch: boolean;
|
|
10
|
+
supportsTranscriptTailing: boolean;
|
|
11
|
+
supportsTmuxAdoption: boolean;
|
|
12
|
+
policyChannel: ProviderPolicyChannel;
|
|
13
|
+
metricsChannel: ProviderMetricsChannel;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderResumeCommandHints {
|
|
17
|
+
/**
|
|
18
|
+
* Command template for standard resume mode.
|
|
19
|
+
* Use "{id}" placeholder for provider session id interpolation.
|
|
20
|
+
*/
|
|
21
|
+
resume: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional command template for fork resume mode.
|
|
24
|
+
*/
|
|
25
|
+
fork?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional UI placeholder for provider session ids.
|
|
28
|
+
*/
|
|
29
|
+
idPlaceholder?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProviderLaunchHints {
|
|
33
|
+
/**
|
|
34
|
+
* Provider-native dangerous mode flags expected to be applied by daemon.
|
|
35
|
+
*/
|
|
36
|
+
dangerousModeFlags: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Optional resume/fork command preview templates for UI.
|
|
39
|
+
*/
|
|
40
|
+
resumeCommands?: ProviderResumeCommandHints;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ProviderPrepareSessionArgs {
|
|
44
|
+
sessionId: string;
|
|
45
|
+
runtimeDir: string;
|
|
46
|
+
socketPath: string;
|
|
47
|
+
secret: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ProviderPrepareSessionResult {
|
|
51
|
+
settingsPath?: string;
|
|
52
|
+
cleanupArtifacts?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ProviderResumeMode = "resume" | "fork";
|
|
56
|
+
|
|
57
|
+
export interface ProviderResumeTarget {
|
|
58
|
+
providerSessionId: string;
|
|
59
|
+
mode?: ProviderResumeMode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ProviderBuildCommandArgs {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
settingsPath?: string;
|
|
65
|
+
providerArgs: string[];
|
|
66
|
+
dangerousMode: boolean;
|
|
67
|
+
providerResume?: ProviderResumeTarget;
|
|
68
|
+
codexHostAccessProfileEnabled?: boolean;
|
|
69
|
+
terminalFeatures?: {
|
|
70
|
+
codexAppServerSpikeEnabled?: boolean;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ProviderDefinition {
|
|
75
|
+
id: ProviderId;
|
|
76
|
+
label: string;
|
|
77
|
+
runtime: "tmux" | "pty";
|
|
78
|
+
capabilities: ProviderCapabilities;
|
|
79
|
+
launchHints?: ProviderLaunchHints;
|
|
80
|
+
prepareSession?: (args: ProviderPrepareSessionArgs) => Promise<ProviderPrepareSessionResult>;
|
|
81
|
+
buildCommand: (args: ProviderBuildCommandArgs) => string[];
|
|
82
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuditLogger - Logs all policy decisions for audit trail and analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Database, GetPolicyDecisionsOptions, PolicyDecisionRecord } from "../db/db";
|
|
6
|
+
import type { PolicyDecision } from "./policyTypes";
|
|
7
|
+
|
|
8
|
+
export interface LogDecisionParams {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
eventId?: number;
|
|
11
|
+
toolName: string;
|
|
12
|
+
args: Record<string, unknown>;
|
|
13
|
+
decision: PolicyDecision;
|
|
14
|
+
timestamp?: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DecisionStatistics {
|
|
18
|
+
total: number;
|
|
19
|
+
allow: number;
|
|
20
|
+
deny: number;
|
|
21
|
+
ask: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class AuditLogger {
|
|
25
|
+
constructor(private db: Database) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Log a policy decision to the audit trail
|
|
29
|
+
*/
|
|
30
|
+
logDecision(params: LogDecisionParams): number {
|
|
31
|
+
return this.db.insertPolicyDecision({
|
|
32
|
+
sessionId: params.sessionId,
|
|
33
|
+
eventId: params.eventId,
|
|
34
|
+
policyId: params.decision.policyId,
|
|
35
|
+
toolName: params.toolName,
|
|
36
|
+
args: params.args,
|
|
37
|
+
decision: params.decision.action,
|
|
38
|
+
reason: params.decision.reason,
|
|
39
|
+
timestamp: params.timestamp,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get policy decisions for a session
|
|
45
|
+
*/
|
|
46
|
+
getDecisions(sessionId: string, options?: GetPolicyDecisionsOptions): PolicyDecisionRecord[] {
|
|
47
|
+
return this.db.getPolicyDecisionsBySessionId(sessionId, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Export decisions in various formats
|
|
52
|
+
*/
|
|
53
|
+
exportDecisions(sessionId: string, format: "json" | "csv"): string {
|
|
54
|
+
const decisions = this.getDecisions(sessionId);
|
|
55
|
+
|
|
56
|
+
if (format === "json") {
|
|
57
|
+
return JSON.stringify(
|
|
58
|
+
decisions.map((d) => ({
|
|
59
|
+
timestamp: d.timestamp.toISOString(),
|
|
60
|
+
toolName: d.toolName,
|
|
61
|
+
decision: d.decision,
|
|
62
|
+
reason: d.reason,
|
|
63
|
+
policyId: d.policyId,
|
|
64
|
+
ruleId: undefined, // Not stored in decision record
|
|
65
|
+
args: d.args,
|
|
66
|
+
eventId: d.eventId,
|
|
67
|
+
})),
|
|
68
|
+
null,
|
|
69
|
+
2
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (format === "csv") {
|
|
74
|
+
const header = "timestamp,toolName,decision,reason,policyId,args\n";
|
|
75
|
+
const rows = decisions
|
|
76
|
+
.map((d) => {
|
|
77
|
+
const timestamp = d.timestamp.toISOString();
|
|
78
|
+
const args = d.args ? JSON.stringify(d.args).replace(/"/g, '""') : "";
|
|
79
|
+
const reason = (d.reason ?? "").replace(/"/g, '""');
|
|
80
|
+
return `${timestamp},${d.toolName},${d.decision},"${reason}",${d.policyId ?? ""},"${args}"`;
|
|
81
|
+
})
|
|
82
|
+
.join("\n");
|
|
83
|
+
|
|
84
|
+
return `${header + rows}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get decision statistics for a session
|
|
92
|
+
*/
|
|
93
|
+
getStatistics(sessionId: string): DecisionStatistics {
|
|
94
|
+
const decisions = this.getDecisions(sessionId);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
total: decisions.length,
|
|
98
|
+
allow: decisions.filter((d) => d.decision === "allow").length,
|
|
99
|
+
deny: decisions.filter((d) => d.decision === "deny").length,
|
|
100
|
+
ask: decisions.filter((d) => d.decision === "ask").length,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|