@cjhyy/code-shell-server 0.9.8 → 0.9.9

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.
Files changed (109) hide show
  1. package/README.md +85 -23
  2. package/dist/bin/code-shell-project-runtime.d.ts +2 -0
  3. package/dist/bin/code-shell-project-runtime.js +8 -0
  4. package/dist/desktop-web/http-api.d.ts +43 -0
  5. package/dist/desktop-web/http-api.js +376 -0
  6. package/dist/hub/approval-lease.d.ts +30 -0
  7. package/dist/hub/approval-lease.js +89 -0
  8. package/dist/hub/auth-http.d.ts +23 -0
  9. package/dist/hub/auth-http.js +259 -0
  10. package/dist/hub/auth-store.d.ts +52 -0
  11. package/dist/hub/auth-store.js +316 -0
  12. package/dist/hub/configuration.d.ts +64 -0
  13. package/dist/hub/configuration.js +555 -0
  14. package/dist/hub/files.d.ts +9 -0
  15. package/dist/hub/files.js +278 -0
  16. package/dist/hub/mcp-configuration.d.ts +51 -0
  17. package/dist/hub/mcp-configuration.js +558 -0
  18. package/dist/hub/mcp-probe.d.ts +24 -0
  19. package/dist/hub/mcp-probe.js +392 -0
  20. package/dist/hub/model-probe.d.ts +20 -0
  21. package/dist/hub/model-probe.js +193 -0
  22. package/dist/hub/outbound-transport.d.ts +36 -0
  23. package/dist/hub/outbound-transport.js +122 -0
  24. package/dist/hub/run-replay.d.ts +42 -0
  25. package/dist/hub/run-replay.js +89 -0
  26. package/dist/hub/session-management.d.ts +36 -0
  27. package/dist/hub/session-management.js +405 -0
  28. package/dist/hub/skills-management.d.ts +43 -0
  29. package/dist/hub/skills-management.js +487 -0
  30. package/dist/hub/uploads.d.ts +22 -0
  31. package/dist/hub/uploads.js +241 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.desktop-web.d.ts +2 -0
  34. package/dist/index.desktop-web.js +2 -0
  35. package/dist/index.js +3 -0
  36. package/dist/index.links.d.ts +6 -0
  37. package/dist/index.links.js +6 -0
  38. package/dist/index.panels.d.ts +9 -0
  39. package/dist/index.panels.js +8 -0
  40. package/dist/index.serve.d.ts +1 -0
  41. package/dist/index.serve.js +1 -0
  42. package/dist/index.storage.d.ts +2 -0
  43. package/dist/index.storage.js +2 -0
  44. package/dist/links/catalog.d.ts +10 -0
  45. package/dist/links/catalog.js +41 -0
  46. package/dist/links/device-oauth.d.ts +52 -0
  47. package/dist/links/device-oauth.js +255 -0
  48. package/dist/links/http.d.ts +34 -0
  49. package/dist/links/http.js +144 -0
  50. package/dist/links/service.d.ts +54 -0
  51. package/dist/links/service.js +578 -0
  52. package/dist/links/types.d.ts +6 -0
  53. package/dist/links/types.js +1 -0
  54. package/dist/mobile-remote/mobile-client-event-validator.js +5 -2
  55. package/dist/mobile-remote/pending-approvals.js +2 -0
  56. package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
  57. package/dist/mobile-remote/remote-host-manager.js +90 -15
  58. package/dist/mobile-remote/room-manager.js +17 -5
  59. package/dist/panels/agent-task-host.d.ts +32 -0
  60. package/dist/panels/agent-task-host.js +457 -0
  61. package/dist/panels/agent-task-models.d.ts +27 -0
  62. package/dist/panels/agent-task-models.js +51 -0
  63. package/dist/panels/agent-task-service.d.ts +105 -0
  64. package/dist/panels/agent-task-service.js +283 -0
  65. package/dist/panels/http.d.ts +34 -0
  66. package/dist/panels/http.js +37 -0
  67. package/dist/panels/hub-binding.d.ts +12 -0
  68. package/dist/panels/hub-binding.js +174 -0
  69. package/dist/panels/management-http.d.ts +29 -0
  70. package/dist/panels/management-http.js +119 -0
  71. package/dist/panels/management.d.ts +39 -0
  72. package/dist/panels/management.js +494 -0
  73. package/dist/panels/process-files.d.ts +14 -0
  74. package/dist/panels/process-files.js +299 -0
  75. package/dist/panels/process-service.d.ts +96 -0
  76. package/dist/panels/process-service.js +501 -0
  77. package/dist/panels/runtime-services.d.ts +24 -0
  78. package/dist/panels/runtime-services.js +462 -0
  79. package/dist/panels/runtime.d.ts +51 -0
  80. package/dist/panels/runtime.js +1063 -0
  81. package/dist/panels/storage-store.d.ts +9 -0
  82. package/dist/panels/storage-store.js +154 -0
  83. package/dist/panels/types.d.ts +52 -0
  84. package/dist/panels/types.js +1 -0
  85. package/dist/project-runtime/control-server.d.ts +19 -0
  86. package/dist/project-runtime/control-server.js +300 -0
  87. package/dist/project-runtime/docker-provider.d.ts +33 -0
  88. package/dist/project-runtime/docker-provider.js +487 -0
  89. package/dist/project-runtime/lock.d.ts +6 -0
  90. package/dist/project-runtime/lock.js +43 -0
  91. package/dist/project-runtime/managed-entry.d.ts +10 -0
  92. package/dist/project-runtime/managed-entry.js +113 -0
  93. package/dist/project-runtime/manager.d.ts +40 -0
  94. package/dist/project-runtime/manager.js +249 -0
  95. package/dist/project-runtime/proxy.d.ts +22 -0
  96. package/dist/project-runtime/proxy.js +706 -0
  97. package/dist/project-runtime/registry.d.ts +54 -0
  98. package/dist/project-runtime/registry.js +286 -0
  99. package/dist/project-runtime/types.d.ts +43 -0
  100. package/dist/project-runtime/types.js +1 -0
  101. package/dist/serve/cli.d.ts +5 -0
  102. package/dist/serve/cli.js +82 -23
  103. package/dist/serve/headless-server.d.ts +11 -0
  104. package/dist/serve/headless-server.js +737 -83
  105. package/dist/session-titles-store.d.ts +17 -0
  106. package/dist/session-titles-store.js +174 -0
  107. package/dist/static-files.js +1 -0
  108. package/dist/worker-bridge-core.js +17 -6
  109. package/package.json +19 -5
@@ -0,0 +1,283 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const MAX_TASKS = 200;
3
+ const MAX_PROMPT_CHARS = 20_000;
4
+ const MAX_RESULT_CHARS = 64_000;
5
+ const MAX_ACTIVITY_ITEMS = 40;
6
+ const MAX_ACTIVITY_MESSAGE_CHARS = 500;
7
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
8
+ const TOOL_NAME = /^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/;
9
+ const TASK_KEY = /^[a-z][a-z0-9_-]{0,63}$/;
10
+ function cleanError(error) {
11
+ return (error instanceof Error ? error.message : String(error || "Task failed")).slice(0, 1_000);
12
+ }
13
+ function failureForReason(reason) {
14
+ switch (reason) {
15
+ case "model_error":
16
+ return "The AI model request failed. Check the selected model and its API credentials.";
17
+ case "prompt_too_long":
18
+ return "The Task prompt exceeded the model context limit.";
19
+ case "max_turns":
20
+ return "The Task reached its turn limit before finishing.";
21
+ case "goal_budget_exhausted":
22
+ return "The Task reached its configured budget before finishing.";
23
+ case "aborted_streaming":
24
+ case "aborted_tools":
25
+ return "The Task was interrupted before finishing.";
26
+ default:
27
+ return reason ? `The Task ended before completion (${reason}).` : "Task failed";
28
+ }
29
+ }
30
+ function publicTask(task) {
31
+ return {
32
+ id: task.id,
33
+ ...(task.key ? { key: task.key } : {}),
34
+ label: task.label,
35
+ status: task.status,
36
+ ...(task.model ? { model: task.model } : {}),
37
+ ...(task.skill ? { skill: task.skill } : {}),
38
+ createdAt: task.createdAt,
39
+ updatedAt: task.updatedAt,
40
+ ...(task.completedAt ? { completedAt: task.completedAt } : {}),
41
+ ...(task.result ? { result: structuredClone(task.result) } : {}),
42
+ ...(task.error ? { error: task.error } : {}),
43
+ ...(task.activity?.length ? { activity: structuredClone(task.activity) } : {}),
44
+ };
45
+ }
46
+ function sameScope(task, owner) {
47
+ return task.owner.appId === owner.appId && task.owner.projectPath === owner.projectPath;
48
+ }
49
+ export class PanelAppAgentTaskService {
50
+ runtime;
51
+ emitChanged;
52
+ now;
53
+ makeId;
54
+ tasks = new Map();
55
+ constructor(runtime, emitChanged, now = Date.now, makeId = randomUUID) {
56
+ this.runtime = runtime;
57
+ this.emitChanged = emitChanged;
58
+ this.now = now;
59
+ this.makeId = makeId;
60
+ }
61
+ start(owner, raw) {
62
+ const prompt = typeof raw.prompt === "string" ? raw.prompt.trim() : "";
63
+ if (!prompt || prompt.length > MAX_PROMPT_CHARS) {
64
+ throw new Error("agent.task.start requires a non-empty prompt up to 20000 characters");
65
+ }
66
+ const label = typeof raw.label === "string" ? raw.label.trim() : "";
67
+ if (!label || label.length > 200) {
68
+ throw new Error("agent.task.start requires a non-empty label up to 200 characters");
69
+ }
70
+ const key = raw.key === undefined ? undefined : String(raw.key);
71
+ if (key !== undefined && !TASK_KEY.test(key)) {
72
+ throw new Error("agent.task.start key must match ^[a-z][a-z0-9_-]{0,63}$");
73
+ }
74
+ const model = raw.model === undefined ? undefined : String(raw.model).trim();
75
+ if (model !== undefined && (!model || model.length > 256 || /[\r\n\0]/.test(model))) {
76
+ throw new Error("agent.task.start model must be a bounded non-empty connection id");
77
+ }
78
+ const skill = raw.skill === undefined ? undefined : String(raw.skill);
79
+ if (skill !== undefined && !owner.availableSkills.includes(skill)) {
80
+ throw new Error(`Task Skill '${skill}' is not bundled with this Panel App`);
81
+ }
82
+ const requestedSkills = raw.skills === undefined ? [] : raw.skills;
83
+ if (!Array.isArray(requestedSkills) ||
84
+ requestedSkills.length > 8 ||
85
+ requestedSkills.some((name) => typeof name !== "string" || !owner.availableSkills.includes(name)))
86
+ throw new Error("Task Skills must be bundled with this Panel App (at most 8)");
87
+ const skillNames = [...new Set([...(skill ? [skill] : []), ...requestedSkills])];
88
+ const requestedTools = raw.toolNames === undefined ? [] : raw.toolNames;
89
+ if (!Array.isArray(requestedTools) ||
90
+ requestedTools.length > 32 ||
91
+ requestedTools.some((name) => typeof name !== "string" || !TOOL_NAME.test(name))) {
92
+ throw new Error("agent.task.start toolNames must contain at most 32 valid tool names");
93
+ }
94
+ const maxTurns = raw.maxTurns === undefined ? 8 : Number(raw.maxTurns);
95
+ if (!Number.isInteger(maxTurns) || maxTurns < 1 || maxTurns > 20) {
96
+ throw new Error("agent.task.start maxTurns must be an integer from 1 to 20");
97
+ }
98
+ const maxContextTokens = raw.maxContextTokens === undefined ? 32_768 : Number(raw.maxContextTokens);
99
+ if (!Number.isInteger(maxContextTokens) ||
100
+ maxContextTokens < 4_096 ||
101
+ maxContextTokens > 65_536) {
102
+ throw new Error("agent.task.start maxContextTokens must be from 4096 to 65536");
103
+ }
104
+ if (key) {
105
+ const existing = [...this.tasks.values()].find((task) => sameScope(task, owner) && task.key === key && !TERMINAL_STATUSES.has(task.status));
106
+ if (existing)
107
+ return publicTask(existing);
108
+ }
109
+ const createdAt = this.now();
110
+ const id = `task-${this.makeId()}`;
111
+ const toolNames = [...new Set(requestedTools)];
112
+ if (skillNames.length && !toolNames.includes("Skill"))
113
+ toolNames.push("Skill");
114
+ const task = {
115
+ id,
116
+ ...(key ? { key } : {}),
117
+ label,
118
+ status: "queued",
119
+ ...(model ? { model } : {}),
120
+ ...(skill ? { skill } : {}),
121
+ createdAt,
122
+ updatedAt: createdAt,
123
+ sessionId: `panel-task-${this.makeId()}`,
124
+ owner: { ...owner, availableSkills: [...owner.availableSkills] },
125
+ prompt,
126
+ toolNames,
127
+ skillNames,
128
+ maxTurns,
129
+ maxContextTokens,
130
+ cancelRequested: false,
131
+ activity: [],
132
+ };
133
+ this.tasks.set(task.id, task);
134
+ this.trimHistory();
135
+ this.emit(task);
136
+ void this.execute(task);
137
+ return publicTask(task);
138
+ }
139
+ list(owner) {
140
+ return [...this.tasks.values()]
141
+ .filter((task) => sameScope(task, owner))
142
+ .sort((a, b) => b.createdAt - a.createdAt)
143
+ .map(publicTask);
144
+ }
145
+ get(owner, id) {
146
+ const task = this.scopedTask(owner, id);
147
+ return publicTask(task);
148
+ }
149
+ async cancel(owner, id) {
150
+ const task = this.scopedTask(owner, id);
151
+ if (TERMINAL_STATUSES.has(task.status))
152
+ return publicTask(task);
153
+ task.cancelRequested = true;
154
+ task.status = "cancelling";
155
+ task.updatedAt = this.now();
156
+ this.emit(task);
157
+ try {
158
+ await this.runtime.cancel(task.sessionId);
159
+ }
160
+ catch (error) {
161
+ task.error = cleanError(error);
162
+ task.updatedAt = this.now();
163
+ this.emit(task);
164
+ }
165
+ return publicTask(task);
166
+ }
167
+ rebind(owner) {
168
+ for (const task of this.tasks.values()) {
169
+ if (!sameScope(task, owner) || TERMINAL_STATUSES.has(task.status))
170
+ continue;
171
+ task.owner = { ...owner, availableSkills: [...owner.availableSkills] };
172
+ this.runtime.rebind(task.sessionId, task.owner);
173
+ }
174
+ }
175
+ cancelApp(appId) {
176
+ for (const task of this.tasks.values()) {
177
+ if (task.owner.appId !== appId || TERMINAL_STATUSES.has(task.status))
178
+ continue;
179
+ task.cancelRequested = true;
180
+ task.status = "cancelling";
181
+ task.updatedAt = this.now();
182
+ this.emit(task);
183
+ void this.runtime.cancel(task.sessionId).catch(() => undefined);
184
+ }
185
+ }
186
+ async execute(task) {
187
+ task.status = "running";
188
+ task.updatedAt = this.now();
189
+ this.emit(task);
190
+ try {
191
+ const result = await this.runtime.run({
192
+ taskId: task.id,
193
+ sessionId: task.sessionId,
194
+ owner: task.owner,
195
+ prompt: task.prompt,
196
+ label: task.label,
197
+ ...(task.model ? { model: task.model } : {}),
198
+ toolNames: task.toolNames,
199
+ skillNames: task.skillNames,
200
+ maxTurns: task.maxTurns,
201
+ maxContextTokens: task.maxContextTokens,
202
+ onProgress: (progress) => this.recordProgress(task, progress),
203
+ });
204
+ task.result = {
205
+ text: String(result.text || "").slice(0, MAX_RESULT_CHARS),
206
+ ...(result.reason ? { reason: result.reason } : {}),
207
+ ...(result.usage ? { usage: result.usage } : {}),
208
+ };
209
+ if (task.cancelRequested) {
210
+ task.status = "cancelled";
211
+ }
212
+ else if (result.reason && result.reason !== "completed") {
213
+ task.status = "failed";
214
+ task.error = cleanError(result.error || result.text || failureForReason(result.reason));
215
+ }
216
+ else {
217
+ task.status = "completed";
218
+ }
219
+ }
220
+ catch (error) {
221
+ task.status = task.cancelRequested ? "cancelled" : "failed";
222
+ task.error = cleanError(error);
223
+ }
224
+ finally {
225
+ task.updatedAt = this.now();
226
+ task.completedAt = task.updatedAt;
227
+ this.emit(task);
228
+ await this.runtime.close(task.sessionId).catch(() => undefined);
229
+ }
230
+ }
231
+ scopedTask(owner, id) {
232
+ if (typeof id !== "string" || !id)
233
+ throw new Error("agent.task requires a task id");
234
+ const task = this.tasks.get(id);
235
+ if (!task || !sameScope(task, owner))
236
+ throw new Error("Task not found for this Panel App");
237
+ return task;
238
+ }
239
+ emit(task) {
240
+ this.emitChanged(task.owner, publicTask(task));
241
+ }
242
+ recordProgress(task, progress) {
243
+ if (TERMINAL_STATUSES.has(task.status))
244
+ return;
245
+ const message = String(progress.message || "")
246
+ .trim()
247
+ .slice(0, MAX_ACTIVITY_MESSAGE_CHARS);
248
+ if (!message)
249
+ return;
250
+ const toolName = progress.toolName?.trim().slice(0, 128);
251
+ const previous = task.activity?.at(-1);
252
+ if (previous?.kind === progress.kind &&
253
+ previous.status === progress.status &&
254
+ previous.message === message &&
255
+ previous.toolName === toolName) {
256
+ return;
257
+ }
258
+ task.activity ??= [];
259
+ task.activity.push({
260
+ kind: progress.kind,
261
+ status: progress.status,
262
+ message,
263
+ ...(toolName ? { toolName } : {}),
264
+ at: this.now(),
265
+ });
266
+ if (task.activity.length > MAX_ACTIVITY_ITEMS) {
267
+ task.activity.splice(0, task.activity.length - MAX_ACTIVITY_ITEMS);
268
+ }
269
+ task.updatedAt = this.now();
270
+ this.emit(task);
271
+ }
272
+ trimHistory() {
273
+ if (this.tasks.size <= MAX_TASKS)
274
+ return;
275
+ for (const task of this.tasks.values()) {
276
+ if (!TERMINAL_STATUSES.has(task.status))
277
+ continue;
278
+ this.tasks.delete(task.id);
279
+ if (this.tasks.size <= MAX_TASKS)
280
+ return;
281
+ }
282
+ }
283
+ }
@@ -0,0 +1,34 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type PanelManagementHttpOptions } from "./management-http.js";
3
+ import { type PanelAgentTaskHostOptions } from "./agent-task-host.js";
4
+ export declare function createPanelHttp(options: PanelManagementHttpOptions & {
5
+ dataDir: string;
6
+ host: "hub" | "desktop";
7
+ publicPathPrefix?: string;
8
+ agentTaskOptions?: PanelAgentTaskHostOptions;
9
+ }): {
10
+ service: {
11
+ snapshot: () => Promise<import("./types.js").PanelSnapshot>;
12
+ assertAuthorized: (context: import("./types.js").PanelOperationContext, expected?: number) => Promise<void>;
13
+ discover(context: import("./types.js").PanelOperationContext, input: unknown): Promise<import("./types.js").PanelDiscovery>;
14
+ preview: (context: import("./types.js").PanelOperationContext, input: unknown) => Promise<import("./types.js").PanelReview>;
15
+ previewUpdate(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelReview>;
16
+ install(context: import("./types.js").PanelOperationContext, token: unknown, bind?: boolean): Promise<{
17
+ id: string;
18
+ }>;
19
+ binding(context: import("./types.js").PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("./types.js").PanelSnapshot>;
20
+ remove(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<{
21
+ removed: true;
22
+ }>;
23
+ cancelOwner(owner: string): void;
24
+ close(): void;
25
+ };
26
+ activeTaskCount: () => number;
27
+ panelAction: (ownerId: string, sessionId: string, input: Record<string, unknown>) => Promise<unknown>;
28
+ invalidate: (appId?: string) => void;
29
+ ownsAssets: (request: IncomingMessage) => boolean;
30
+ handleAssets: (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
31
+ handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
32
+ cancelOwner(owner: string): void;
33
+ close(): void;
34
+ };
@@ -0,0 +1,37 @@
1
+ import { createPanelManagementHttp } from "./management-http.js";
2
+ import { createPanelRuntime, panelWebCompatibility } from "./runtime.js";
3
+ import { createPanelAgentTaskHost } from "./agent-task-host.js";
4
+ export function createPanelHttp(options) {
5
+ const management = createPanelManagementHttp({
6
+ ...options,
7
+ compatibility: options.compatibility ?? panelWebCompatibility,
8
+ onChanged: async (id, kind) => {
9
+ runtime?.invalidate(id);
10
+ await options.onChanged?.(id, kind);
11
+ },
12
+ });
13
+ const runtime = createPanelRuntime({
14
+ ...options,
15
+ snapshot: management.service.snapshot,
16
+ createAgentTasks: (hooks) => createPanelAgentTaskHost({ ...options.agentTaskOptions, ...hooks }),
17
+ });
18
+ return {
19
+ service: management.service,
20
+ activeTaskCount: runtime.activeTaskCount,
21
+ panelAction: runtime.panelAction,
22
+ invalidate: runtime.invalidate,
23
+ ownsAssets: runtime.ownsAssets,
24
+ handleAssets: runtime.handleAssets,
25
+ async handle(request, response) {
26
+ return (await runtime.handle(request, response)) || management.handle(request, response);
27
+ },
28
+ cancelOwner(owner) {
29
+ runtime.cancelOwner(owner);
30
+ management.cancelOwner(owner);
31
+ },
32
+ close() {
33
+ runtime.close();
34
+ management.close();
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,12 @@
1
+ export interface HubPanelBinding {
2
+ bindingCwd: string;
3
+ /** Cheap synchronous topology check; never derives a new target after startup. */
4
+ assertBinding: () => void;
5
+ }
6
+ /**
7
+ * --cwd authorizes that workspace, not an arbitrary path written into .git.
8
+ * Verify Git's forward and reverse worktree relationship once, then freeze the
9
+ * binding target shared by management, storage, and Core's Skill scanner.
10
+ * An unsupported or changed topology disables panels without stopping the Hub.
11
+ */
12
+ export declare function createHubPanelBinding(cwdInput: string): HubPanelBinding;
@@ -0,0 +1,174 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync, realpathSync, } from "node:fs";
3
+ import { devNull } from "node:os";
4
+ import { basename, dirname, join, resolve, sep } from "node:path";
5
+ import { resolvePanelAppBindingProjectPath } from "@cjhyy/code-shell-core";
6
+ import { PanelManagementError } from "./management.js";
7
+ function refused() {
8
+ throw new PanelManagementError(403, "panel_binding_unavailable", "无法核验面板所属项目,或 Git 工作区关联已经变化。请检查工作区后重启服务,再绑定面板。");
9
+ }
10
+ function metadata(file) {
11
+ try {
12
+ return lstatSync(file);
13
+ }
14
+ catch (error) {
15
+ if (error.code === "ENOENT")
16
+ return;
17
+ throw error;
18
+ }
19
+ }
20
+ function readPointer(file) {
21
+ const descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
22
+ try {
23
+ const info = fstatSync(descriptor);
24
+ if (!info.isFile() || info.size > 8192)
25
+ refused();
26
+ const content = readFileSync(descriptor, "utf8");
27
+ if (Buffer.byteLength(content) > 8192 || content.includes("\0"))
28
+ refused();
29
+ return { content, identity: `${info.dev}:${info.ino}` };
30
+ }
31
+ finally {
32
+ closeSync(descriptor);
33
+ }
34
+ }
35
+ function nearestGit(cwd) {
36
+ let current = cwd;
37
+ for (let depth = 0; depth < 256; depth++) {
38
+ const path = join(current, ".git");
39
+ const info = metadata(path);
40
+ if (info) {
41
+ if (info.isSymbolicLink() || (!info.isDirectory() && !info.isFile()))
42
+ refused();
43
+ return { root: current, path, info };
44
+ }
45
+ const parent = dirname(current);
46
+ if (parent === current)
47
+ return;
48
+ current = parent;
49
+ }
50
+ return refused();
51
+ }
52
+ function sameIdentity(actual, expected) {
53
+ return actual.dev === expected.dev && actual.ino === expected.ino;
54
+ }
55
+ function directoryGuard(path) {
56
+ const canonical = realpathSync(path);
57
+ const info = lstatSync(canonical);
58
+ if (!info.isDirectory() || info.isSymbolicLink())
59
+ refused();
60
+ return () => {
61
+ if (realpathSync(path) !== canonical)
62
+ refused();
63
+ const current = lstatSync(canonical);
64
+ if (!current.isDirectory() || current.isSymbolicLink() || !sameIdentity(current, info))
65
+ refused();
66
+ };
67
+ }
68
+ function pointerGuard(path) {
69
+ const initial = readPointer(path);
70
+ return () => {
71
+ const current = readPointer(path);
72
+ if (current.content !== initial.content || current.identity !== initial.identity)
73
+ refused();
74
+ };
75
+ }
76
+ /**
77
+ * --cwd authorizes that workspace, not an arbitrary path written into .git.
78
+ * Verify Git's forward and reverse worktree relationship once, then freeze the
79
+ * binding target shared by management, storage, and Core's Skill scanner.
80
+ * An unsupported or changed topology disables panels without stopping the Hub.
81
+ */
82
+ export function createHubPanelBinding(cwdInput) {
83
+ const cwd = resolve(cwdInput);
84
+ try {
85
+ const guards = [directoryGuard(cwd)];
86
+ const initial = nearestGit(cwd);
87
+ let bindingCwd = cwd;
88
+ if (initial) {
89
+ const environment = { ...process.env };
90
+ for (const key of Object.keys(environment))
91
+ if (key.startsWith("GIT_"))
92
+ delete environment[key];
93
+ environment.GIT_CONFIG_NOSYSTEM = "1";
94
+ environment.GIT_CONFIG_GLOBAL = devNull;
95
+ environment.GIT_OPTIONAL_LOCKS = "0";
96
+ const git = (at, ...args) => execFileSync("git", ["-c", "core.fsmonitor=false", "-C", at, ...args], {
97
+ env: environment,
98
+ encoding: "utf8",
99
+ timeout: 3000,
100
+ maxBuffer: 256 * 1024,
101
+ stdio: ["ignore", "pipe", "pipe"],
102
+ }).trimEnd();
103
+ const top = realpathSync(git(cwd, "rev-parse", "--show-toplevel"));
104
+ const gitDir = realpathSync(git(cwd, "rev-parse", "--absolute-git-dir"));
105
+ const common = realpathSync(resolve(cwd, git(cwd, "rev-parse", "--git-common-dir")));
106
+ if (top !== realpathSync(initial.root))
107
+ refused();
108
+ bindingCwd = resolvePanelAppBindingProjectPath(cwd);
109
+ guards.push(directoryGuard(initial.root), directoryGuard(common));
110
+ if (initial.info.isDirectory()) {
111
+ if (gitDir !== realpathSync(initial.path) ||
112
+ common !== gitDir ||
113
+ realpathSync(bindingCwd) !== top)
114
+ refused();
115
+ guards.push(directoryGuard(initial.path));
116
+ }
117
+ else {
118
+ // Standard linked worktrees must point into the main repository's own
119
+ // registry, whose back-pointer names this exact .git file.
120
+ if (basename(common) !== ".git")
121
+ refused();
122
+ const main = dirname(common);
123
+ const relativeGitDir = gitDir.slice(common.length + 1);
124
+ if (!gitDir.startsWith(common + sep) ||
125
+ !/^worktrees[\\/][^\\/]+$/.test(relativeGitDir) ||
126
+ realpathSync(bindingCwd) !== realpathSync(main))
127
+ refused();
128
+ if (realpathSync(git(main, "rev-parse", "--show-toplevel")) !== realpathSync(main))
129
+ refused();
130
+ if (realpathSync(git(main, "rev-parse", "--absolute-git-dir")) !== common)
131
+ refused();
132
+ const backPointer = join(gitDir, "gitdir");
133
+ const commonPointer = join(gitDir, "commondir");
134
+ if (realpathSync(readPointer(backPointer).content.trim()) !== realpathSync(initial.path))
135
+ refused();
136
+ if (realpathSync(resolve(gitDir, readPointer(commonPointer).content.trim())) !== common)
137
+ refused();
138
+ const listed = git(main, "worktree", "list", "--porcelain", "-z")
139
+ .split("\0")
140
+ .filter((line) => line.startsWith("worktree "))
141
+ .map((line) => line.slice(9));
142
+ if (!listed.includes(top) || !listed.includes(realpathSync(main)))
143
+ refused();
144
+ guards.push(directoryGuard(main), directoryGuard(gitDir), pointerGuard(initial.path), pointerGuard(backPointer), pointerGuard(commonPointer));
145
+ }
146
+ }
147
+ const frozen = bindingCwd;
148
+ const assertBinding = () => {
149
+ try {
150
+ for (const check of guards)
151
+ check();
152
+ const current = nearestGit(cwd);
153
+ if (initial) {
154
+ if (!current ||
155
+ current.path !== initial.path ||
156
+ !sameIdentity(current.info, initial.info))
157
+ refused();
158
+ }
159
+ else if (current && (current.root !== cwd || !current.info.isDirectory()))
160
+ refused();
161
+ if (resolvePanelAppBindingProjectPath(cwd) !== frozen)
162
+ refused();
163
+ }
164
+ catch {
165
+ refused();
166
+ }
167
+ };
168
+ assertBinding();
169
+ return { bindingCwd: frozen, assertBinding };
170
+ }
171
+ catch {
172
+ return { bindingCwd: cwd, assertBinding: refused };
173
+ }
174
+ }
@@ -0,0 +1,29 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type PanelManagementOptions } from "./management.js";
3
+ import type { PanelOperationContext } from "./types.js";
4
+ export interface PanelManagementHttpOptions extends PanelManagementOptions {
5
+ ownerId: (request: IncomingMessage) => Promise<string | undefined>;
6
+ isAuthorized: (request: IncomingMessage) => Promise<boolean>;
7
+ }
8
+ /** Call only after the host's same-origin authentication boundary. */
9
+ export declare function createPanelManagementHttp(options: PanelManagementHttpOptions): {
10
+ service: {
11
+ snapshot: () => Promise<import("./types.js").PanelSnapshot>;
12
+ assertAuthorized: (context: PanelOperationContext, expected?: number) => Promise<void>;
13
+ discover(context: PanelOperationContext, input: unknown): Promise<import("./types.js").PanelDiscovery>;
14
+ preview: (context: PanelOperationContext, input: unknown) => Promise<import("./types.js").PanelReview>;
15
+ previewUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelReview>;
16
+ install(context: PanelOperationContext, token: unknown, bind?: boolean): Promise<{
17
+ id: string;
18
+ }>;
19
+ binding(context: PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("./types.js").PanelSnapshot>;
20
+ remove(context: PanelOperationContext, id: unknown, expected: unknown): Promise<{
21
+ removed: true;
22
+ }>;
23
+ cancelOwner(owner: string): void;
24
+ close(): void;
25
+ };
26
+ close: () => void;
27
+ cancelOwner: (owner: string) => void;
28
+ handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
29
+ };