heyhank 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/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { networkInterfaces } from "node:os";
|
|
6
|
+
|
|
7
|
+
const AUTH_FILE = join(homedir(), ".heyhank", "auth.json");
|
|
8
|
+
const TOKEN_BYTES = 32; // 64 hex characters
|
|
9
|
+
|
|
10
|
+
interface AuthData {
|
|
11
|
+
token: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let cachedToken: string | null = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the auth token. Priority:
|
|
19
|
+
* 1. HEYHANK_AUTH_TOKEN env var (falls back to COMPANION_AUTH_TOKEN)
|
|
20
|
+
* 2. Persisted token from ~/.heyhank/auth.json
|
|
21
|
+
* 3. Auto-generate and persist a new token
|
|
22
|
+
*/
|
|
23
|
+
export function getToken(): string {
|
|
24
|
+
// Env var override (always takes priority)
|
|
25
|
+
const envToken = process.env.HEYHANK_AUTH_TOKEN || process.env.COMPANION_AUTH_TOKEN;
|
|
26
|
+
if (envToken && envToken.trim()) {
|
|
27
|
+
cachedToken = envToken.trim();
|
|
28
|
+
return cachedToken;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Return cached token if available
|
|
32
|
+
if (cachedToken) return cachedToken;
|
|
33
|
+
|
|
34
|
+
// Try reading from file
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(AUTH_FILE)) {
|
|
37
|
+
const raw = readFileSync(AUTH_FILE, "utf-8");
|
|
38
|
+
const data = JSON.parse(raw) as Partial<AuthData>;
|
|
39
|
+
if (typeof data.token === "string" && data.token.length >= 32) {
|
|
40
|
+
cachedToken = data.token;
|
|
41
|
+
return cachedToken;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// File corrupt or unreadable — generate new
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Generate new token
|
|
49
|
+
const token = randomBytes(TOKEN_BYTES).toString("hex");
|
|
50
|
+
const data: AuthData = { token, createdAt: Date.now() };
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(dirname(AUTH_FILE), { recursive: true });
|
|
53
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error("[auth] Failed to persist auth token:", err);
|
|
56
|
+
}
|
|
57
|
+
cachedToken = token;
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Verify a candidate token using constant-time comparison.
|
|
63
|
+
*/
|
|
64
|
+
export function verifyToken(candidate: string | null | undefined): boolean {
|
|
65
|
+
if (!candidate) return false;
|
|
66
|
+
const expected = getToken();
|
|
67
|
+
const candidateBuf = Buffer.from(candidate);
|
|
68
|
+
const expectedBuf = Buffer.from(expected);
|
|
69
|
+
if (candidateBuf.length !== expectedBuf.length) return false;
|
|
70
|
+
return timingSafeEqual(candidateBuf, expectedBuf);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the primary LAN IP address for QR code URL generation.
|
|
75
|
+
* Falls back to "localhost" if no LAN IP is found.
|
|
76
|
+
*/
|
|
77
|
+
export function getLanAddress(): string {
|
|
78
|
+
const interfaces = networkInterfaces();
|
|
79
|
+
for (const name of Object.keys(interfaces)) {
|
|
80
|
+
const addrs = interfaces[name];
|
|
81
|
+
if (!addrs) continue;
|
|
82
|
+
for (const addr of addrs) {
|
|
83
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
84
|
+
return addr.address;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return "localhost";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get all available access addresses: localhost, LAN IP, and Tailscale IP.
|
|
93
|
+
* Tailscale uses 100.x.x.x addresses (CGNAT range) on utun / tailscale interfaces.
|
|
94
|
+
*/
|
|
95
|
+
export function getAllAddresses(): { label: string; ip: string }[] {
|
|
96
|
+
const result: { label: string; ip: string }[] = [
|
|
97
|
+
{ label: "Localhost", ip: "localhost" },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const interfaces = networkInterfaces();
|
|
101
|
+
let lanIp: string | null = null;
|
|
102
|
+
let tailscaleIp: string | null = null;
|
|
103
|
+
|
|
104
|
+
for (const name of Object.keys(interfaces)) {
|
|
105
|
+
const addrs = interfaces[name];
|
|
106
|
+
if (!addrs) continue;
|
|
107
|
+
for (const addr of addrs) {
|
|
108
|
+
if (addr.family !== "IPv4" || addr.internal) continue;
|
|
109
|
+
|
|
110
|
+
// Tailscale uses 100.64.0.0/10 (CGNAT) — detect by IP range
|
|
111
|
+
if (addr.address.startsWith("100.")) {
|
|
112
|
+
const second = parseInt(addr.address.split(".")[1], 10);
|
|
113
|
+
if (second >= 64 && second <= 127) {
|
|
114
|
+
tailscaleIp = addr.address;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!lanIp) lanIp = addr.address;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (lanIp) result.push({ label: "LAN", ip: lanIp });
|
|
124
|
+
if (tailscaleIp) result.push({ label: "Tailscale", ip: tailscaleIp });
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Regenerate the auth token — creates a new random token, persists it,
|
|
131
|
+
* and returns the new value. Existing sessions using the old token will
|
|
132
|
+
* be invalidated on their next request.
|
|
133
|
+
*/
|
|
134
|
+
export function regenerateToken(): string {
|
|
135
|
+
const token = randomBytes(TOKEN_BYTES).toString("hex");
|
|
136
|
+
const data: AuthData = { token, createdAt: Date.now() };
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(dirname(AUTH_FILE), { recursive: true });
|
|
139
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error("[auth] Failed to persist regenerated token:", err);
|
|
142
|
+
}
|
|
143
|
+
cachedToken = token;
|
|
144
|
+
return token;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Reset cached state — for testing only */
|
|
148
|
+
export function _resetForTest(): void {
|
|
149
|
+
cachedToken = null;
|
|
150
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ─── Auto-Approve System ─────────────────────────────────────────────────────
|
|
2
|
+
// Agent Max 2.0 can automatically approve/deny permission requests
|
|
3
|
+
// for trusted agents based on risk assessment rules.
|
|
4
|
+
|
|
5
|
+
import type { AgentConfig } from "./agent-types.js";
|
|
6
|
+
import * as agentStore from "./agent-store.js";
|
|
7
|
+
import { isKilled } from "./kill-switch.js";
|
|
8
|
+
|
|
9
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface AutoApproveRule {
|
|
12
|
+
/** Agent ID this rule applies to ("*" for all) */
|
|
13
|
+
agentId: string;
|
|
14
|
+
/** Tool names to auto-approve (empty = all) */
|
|
15
|
+
allowedTools: string[];
|
|
16
|
+
/** Tool names to always deny */
|
|
17
|
+
deniedTools: string[];
|
|
18
|
+
/** Whether to auto-approve all safe verdicts */
|
|
19
|
+
autoApproveSafe: boolean;
|
|
20
|
+
/** Whether to auto-deny all dangerous verdicts */
|
|
21
|
+
autoDenyDangerous: boolean;
|
|
22
|
+
/** Max cost per action before requiring manual approval (USD) */
|
|
23
|
+
maxCostPerAction?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PermissionEvaluation {
|
|
27
|
+
action: "approve" | "deny" | "manual";
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Default Rules ───────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const DEFAULT_RULES: AutoApproveRule[] = [
|
|
34
|
+
{
|
|
35
|
+
// Monitoring Agent: read-only, always approve
|
|
36
|
+
agentId: "monitoring-agent",
|
|
37
|
+
allowedTools: ["bash", "read", "glob", "grep"],
|
|
38
|
+
deniedTools: ["write", "edit"],
|
|
39
|
+
autoApproveSafe: true,
|
|
40
|
+
autoDenyDangerous: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
// Coding Agent: full access, approve safe, manual for dangerous
|
|
44
|
+
agentId: "coding-agent",
|
|
45
|
+
allowedTools: [],
|
|
46
|
+
deniedTools: [],
|
|
47
|
+
autoApproveSafe: true,
|
|
48
|
+
autoDenyDangerous: false,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
// Default: approve safe, deny dangerous
|
|
52
|
+
agentId: "*",
|
|
53
|
+
allowedTools: [],
|
|
54
|
+
deniedTools: [],
|
|
55
|
+
autoApproveSafe: true,
|
|
56
|
+
autoDenyDangerous: true,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// ─── Dangerous Patterns ──────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const DANGEROUS_BASH_PATTERNS = [
|
|
63
|
+
/rm\s+-rf?\s+\//, // rm -rf /
|
|
64
|
+
/mkfs/, // format disk
|
|
65
|
+
/dd\s+if=/, // dd
|
|
66
|
+
/>\s*\/dev\/sd/, // write to disk device
|
|
67
|
+
/chmod\s+777/, // world-writable
|
|
68
|
+
/curl.*\|\s*(ba)?sh/, // curl | bash
|
|
69
|
+
/wget.*\|\s*(ba)?sh/, // wget | bash
|
|
70
|
+
/:(){ :\|:& };:/, // fork bomb
|
|
71
|
+
/--force.*push/, // force push
|
|
72
|
+
/git\s+push.*--force/, // git force push
|
|
73
|
+
/DROP\s+(TABLE|DATABASE)/i, // SQL drop
|
|
74
|
+
/DELETE\s+FROM.*WHERE\s+1/i, // SQL mass delete
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const SAFE_READ_ONLY_TOOLS = new Set([
|
|
78
|
+
"Read", "Glob", "Grep", "WebSearch", "WebFetch",
|
|
79
|
+
"TaskGet", "TaskList",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// ─── Evaluation ──────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** Evaluate whether to auto-approve a permission request. */
|
|
85
|
+
export function evaluate(
|
|
86
|
+
agentId: string,
|
|
87
|
+
toolName: string,
|
|
88
|
+
toolInput: Record<string, unknown>,
|
|
89
|
+
aiVerdict?: "safe" | "dangerous" | "uncertain",
|
|
90
|
+
): PermissionEvaluation {
|
|
91
|
+
// Kill switch overrides everything
|
|
92
|
+
if (isKilled()) {
|
|
93
|
+
return { action: "deny", reason: "Kill switch active" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Find matching rule (specific agent first, then wildcard)
|
|
97
|
+
const rule =
|
|
98
|
+
DEFAULT_RULES.find((r) => r.agentId === agentId) ||
|
|
99
|
+
DEFAULT_RULES.find((r) => r.agentId === "*");
|
|
100
|
+
|
|
101
|
+
if (!rule) {
|
|
102
|
+
return { action: "manual", reason: "No matching rule" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check denied tools
|
|
106
|
+
if (rule.deniedTools.length > 0 && rule.deniedTools.includes(toolName.toLowerCase())) {
|
|
107
|
+
return { action: "deny", reason: `Tool "${toolName}" is denied for agent` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check allowed tools (if specified, only these are allowed)
|
|
111
|
+
if (rule.allowedTools.length > 0 && !rule.allowedTools.includes(toolName.toLowerCase())) {
|
|
112
|
+
return { action: "deny", reason: `Tool "${toolName}" not in allowed list` };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Read-only tools are always safe
|
|
116
|
+
if (SAFE_READ_ONLY_TOOLS.has(toolName)) {
|
|
117
|
+
return { action: "approve", reason: "Read-only tool" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check bash commands for dangerous patterns
|
|
121
|
+
if (toolName.toLowerCase() === "bash" || toolName === "Bash") {
|
|
122
|
+
const command = String(toolInput?.command || toolInput?.cmd || "");
|
|
123
|
+
for (const pattern of DANGEROUS_BASH_PATTERNS) {
|
|
124
|
+
if (pattern.test(command)) {
|
|
125
|
+
return { action: "deny", reason: `Dangerous bash pattern: ${pattern}` };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Use AI verdict if available
|
|
131
|
+
if (aiVerdict === "safe" && rule.autoApproveSafe) {
|
|
132
|
+
return { action: "approve", reason: "AI verdict: safe" };
|
|
133
|
+
}
|
|
134
|
+
if (aiVerdict === "dangerous" && rule.autoDenyDangerous) {
|
|
135
|
+
return { action: "deny", reason: "AI verdict: dangerous" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Default to manual review
|
|
139
|
+
return { action: "manual", reason: "Requires manual review" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Check if an agent session should use auto-approve. */
|
|
143
|
+
export function shouldAutoApprove(agentId: string): boolean {
|
|
144
|
+
const agent = agentStore.getAgent(agentId);
|
|
145
|
+
if (!agent) return false;
|
|
146
|
+
// Auto-approve for agents running with bypassPermissions
|
|
147
|
+
return agent.permissionMode === "bypassPermissions";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Get all rules (for admin UI). */
|
|
151
|
+
export function getRules(): AutoApproveRule[] {
|
|
152
|
+
return [...DEFAULT_RULES];
|
|
153
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { callInternalAI } from "./internal-ai.js";
|
|
2
|
+
|
|
3
|
+
function sanitizeTitle(raw: string): string | null {
|
|
4
|
+
const title = raw.replace(/^"|"$/g, "").replace(/^'|'$/g, "").trim();
|
|
5
|
+
if (!title || title.length >= 100) return null;
|
|
6
|
+
return title;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a short session title using the configured internal AI provider.
|
|
11
|
+
* Returns null if no provider is configured or if generation fails.
|
|
12
|
+
*/
|
|
13
|
+
export async function generateSessionTitle(
|
|
14
|
+
firstUserMessage: string,
|
|
15
|
+
_model: string,
|
|
16
|
+
options?: {
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
},
|
|
19
|
+
): Promise<string | null> {
|
|
20
|
+
const truncated = firstUserMessage.slice(0, 500);
|
|
21
|
+
const userPrompt = `Generate a concise 3-5 word session title for this user request. Output only the title.\n\nRequest: ${truncated}`;
|
|
22
|
+
|
|
23
|
+
const result = await callInternalAI({
|
|
24
|
+
userPrompt,
|
|
25
|
+
maxTokens: 256,
|
|
26
|
+
temperature: 0.2,
|
|
27
|
+
timeoutMs: options?.timeoutMs || 15_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
console.warn(`[auto-namer] Failed to generate title: ${result.error}`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return sanitizeTitle(result.text);
|
|
36
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BrowserIncomingMessage, BrowserOutgoingMessage } from "./session-types.js";
|
|
2
|
+
|
|
3
|
+
// ─── Unified Backend Adapter Interface ───────────────────────────────────────
|
|
4
|
+
// Both Claude Code (NDJSON WebSocket) and Codex (JSON-RPC stdio/WS) implement
|
|
5
|
+
// this so that application code never branches on BackendType for message routing.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unified interface for backend communication.
|
|
9
|
+
*
|
|
10
|
+
* Adapters translate between the backend-native protocol and the common
|
|
11
|
+
* BrowserIncomingMessage / BrowserOutgoingMessage types used by the bridge
|
|
12
|
+
* and the frontend.
|
|
13
|
+
*/
|
|
14
|
+
export interface IBackendAdapter {
|
|
15
|
+
/** Send a browser-originated message to the backend. Returns true if accepted. */
|
|
16
|
+
send(msg: BrowserOutgoingMessage): boolean;
|
|
17
|
+
|
|
18
|
+
/** Whether the backend transport is currently connected and ready. */
|
|
19
|
+
isConnected(): boolean;
|
|
20
|
+
|
|
21
|
+
/** Gracefully disconnect the backend transport. */
|
|
22
|
+
disconnect(): Promise<void>;
|
|
23
|
+
|
|
24
|
+
// ── Event registration (called once at attachment time) ──
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register callback for messages to forward to browsers.
|
|
28
|
+
* The adapter translates backend-native protocol into BrowserIncomingMessage.
|
|
29
|
+
*/
|
|
30
|
+
onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register callback for session metadata updates (CLI session ID, model, cwd).
|
|
34
|
+
* Used for --resume tracking and state synchronization.
|
|
35
|
+
*/
|
|
36
|
+
onSessionMeta(cb: (meta: { cliSessionId?: string; model?: string; cwd?: string }) => void): void;
|
|
37
|
+
|
|
38
|
+
/** Register callback for transport disconnection. */
|
|
39
|
+
onDisconnect(cb: () => void): void;
|
|
40
|
+
|
|
41
|
+
/** Register callback for initialization errors. */
|
|
42
|
+
onInitError?(cb: (error: string) => void): void;
|
|
43
|
+
|
|
44
|
+
// ── Optional capabilities (not all backends support these) ──
|
|
45
|
+
|
|
46
|
+
/** Return backend-specific rate limits, if available (Codex only). */
|
|
47
|
+
getRateLimits?(): {
|
|
48
|
+
primary: { usedPercent: number; windowDurationMins: number; resetsAt: number } | null;
|
|
49
|
+
secondary: { usedPercent: number; windowDurationMins: number; resetsAt: number } | null;
|
|
50
|
+
} | null;
|
|
51
|
+
|
|
52
|
+
/** Handle transport-level close (used when WS proxy drops). */
|
|
53
|
+
handleTransportClose?(): void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hono middleware that sets Cache-Control headers for production static assets.
|
|
5
|
+
*
|
|
6
|
+
* Must be registered BEFORE serveStatic so it can modify the response headers
|
|
7
|
+
* after the static file is served (via await next()).
|
|
8
|
+
*
|
|
9
|
+
* Header strategy:
|
|
10
|
+
* - sw.js / workbox-*.js: no-cache (browser must revalidate on each load)
|
|
11
|
+
* - index.html: no-cache (fresh HTML triggers SW update detection)
|
|
12
|
+
* - manifest.json: no-cache (browser checks on each visit)
|
|
13
|
+
* - /assets/*: immutable, 1 year (Vite content-hashed filenames)
|
|
14
|
+
* - /fonts/*.woff2: immutable, 1 year (stable font files)
|
|
15
|
+
* - Icons/images: 1 day cache
|
|
16
|
+
*/
|
|
17
|
+
export function cacheControlMiddleware(): MiddlewareHandler {
|
|
18
|
+
return async (c, next) => {
|
|
19
|
+
await next();
|
|
20
|
+
|
|
21
|
+
// Only set cache headers on successful responses
|
|
22
|
+
if (c.res.status !== 200) return;
|
|
23
|
+
|
|
24
|
+
const path = c.req.path;
|
|
25
|
+
|
|
26
|
+
// Service worker files: browsers must always revalidate
|
|
27
|
+
if (path === "/sw.js" || path.startsWith("/workbox-")) {
|
|
28
|
+
c.header("Cache-Control", "no-cache");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// index.html (served for / and /index.html): must be fresh
|
|
33
|
+
if (path === "/" || path === "/index.html") {
|
|
34
|
+
c.header("Cache-Control", "no-cache");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// manifest.json: must be fresh
|
|
39
|
+
if (path === "/manifest.json") {
|
|
40
|
+
c.header("Cache-Control", "no-cache");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Vite hashed assets: immutable (filename changes on content change)
|
|
45
|
+
if (path.startsWith("/assets/")) {
|
|
46
|
+
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Font files: immutable
|
|
51
|
+
if (path.startsWith("/fonts/") && path.endsWith(".woff2")) {
|
|
52
|
+
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Other static files (icons, images): cache for 1 day
|
|
57
|
+
if (path.endsWith(".png") || path.endsWith(".svg") || path.endsWith(".ico")) {
|
|
58
|
+
c.header("Cache-Control", "public, max-age=86400");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|