@aliceshimada/mica 1.0.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/SECURITY.md ADDED
@@ -0,0 +1,22 @@
1
+ # Security Policy
2
+
3
+ ## Supported scope
4
+
5
+ MICA is designed for local control of already-open Wolfram Desktop / Mathematica notebooks. Wolfram Desktop / Mathematica 14.1+ is supported; Mathematica 13.x / 14.0 remains experimental. Headless Wolfram Engine is not supported for live notebook control.
6
+
7
+ ## Security assumptions
8
+
9
+ - The HTTP bridge binds to `127.0.0.1` / localhost by default.
10
+ - MICA has no remote mode. Do not expose the bridge port to a network.
11
+ - Protocol endpoints require bearer token auth with the generated local session token.
12
+ - The dashboard token is carried in the URL fragment (`#token=...`) and should be treated as sensitive for the current user session.
13
+ - Notebook mutation permissions are explicit. Insert, modify, delete, run, and save operations are controlled by the Wolfram permission block installed for the bridge.
14
+ - MICA does not provide an arbitrary shell tool or a raw-eval MCP endpoint.
15
+
16
+ ## Reporting vulnerabilities
17
+
18
+ Report security issues through the GitHub repository security contact or issues page:
19
+
20
+ https://github.com/Alice-Shimada/mica/issues
21
+
22
+ Please avoid including sensitive notebook content, bearer tokens, or local session files in public reports.
@@ -0,0 +1,115 @@
1
+ export class AgentRegistry {
2
+ agents = new Map();
3
+ register(input) {
4
+ const next = {
5
+ agentSessionId: input.agentSessionId,
6
+ wolframVersion: input.wolframVersion,
7
+ platform: input.platform,
8
+ lastSeenAt: input.seenAt,
9
+ degradedAt: undefined,
10
+ degraded: false,
11
+ offlineAt: undefined,
12
+ offline: false,
13
+ retired: false,
14
+ retiredReason: undefined,
15
+ status: "live",
16
+ machineId: input.machineId,
17
+ frontendSessionId: input.frontendSessionId,
18
+ wolframProcessId: input.wolframProcessId,
19
+ };
20
+ this.agents.set(input.agentSessionId, next);
21
+ return this.clone(next);
22
+ }
23
+ heartbeat(agentSessionId, seenAt) {
24
+ const existing = this.agents.get(agentSessionId);
25
+ if (!existing)
26
+ return undefined;
27
+ if (existing.retired)
28
+ return undefined;
29
+ const next = {
30
+ ...existing,
31
+ lastSeenAt: seenAt,
32
+ degradedAt: undefined,
33
+ degraded: false,
34
+ offlineAt: undefined,
35
+ offline: false,
36
+ retired: false,
37
+ retiredReason: undefined,
38
+ status: "live",
39
+ };
40
+ this.agents.set(agentSessionId, next);
41
+ return this.clone(next);
42
+ }
43
+ retire(agentSessionId, retiredAt, reason = "no_live_notebooks") {
44
+ const existing = this.agents.get(agentSessionId);
45
+ if (!existing || existing.retired)
46
+ return existing ? this.clone(existing) : undefined;
47
+ const next = {
48
+ ...existing,
49
+ degraded: false,
50
+ degradedAt: undefined,
51
+ offline: true,
52
+ retired: true,
53
+ retiredReason: reason,
54
+ offlineAt: retiredAt,
55
+ status: "retired",
56
+ };
57
+ this.agents.set(agentSessionId, next);
58
+ return this.clone(next);
59
+ }
60
+ get(agentSessionId) {
61
+ const existing = this.agents.get(agentSessionId);
62
+ return existing ? this.clone(existing) : undefined;
63
+ }
64
+ list() {
65
+ return [...this.agents.values()]
66
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt)
67
+ .map((agent) => this.clone(agent));
68
+ }
69
+ hasLiveAgent() {
70
+ for (const agent of this.agents.values()) {
71
+ if (!agent.offline && !agent.retired)
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+ markOfflineOlderThan(now, maxAgeMs) {
77
+ const newlyOffline = [];
78
+ for (const agent of this.agents.values()) {
79
+ if (agent.offline || agent.retired)
80
+ continue;
81
+ if (now - agent.lastSeenAt < maxAgeMs)
82
+ continue;
83
+ this.agents.set(agent.agentSessionId, {
84
+ ...agent,
85
+ degraded: false,
86
+ degradedAt: undefined,
87
+ offline: true,
88
+ offlineAt: now,
89
+ status: "offline",
90
+ });
91
+ newlyOffline.push(agent.agentSessionId);
92
+ }
93
+ return newlyOffline;
94
+ }
95
+ markDegradedOlderThan(now, maxAgeMs) {
96
+ const newlyDegraded = [];
97
+ for (const agent of this.agents.values()) {
98
+ if (agent.degraded || agent.offline || agent.retired)
99
+ continue;
100
+ if (now - agent.lastSeenAt < maxAgeMs)
101
+ continue;
102
+ this.agents.set(agent.agentSessionId, {
103
+ ...agent,
104
+ degraded: true,
105
+ degradedAt: now,
106
+ status: "degraded",
107
+ });
108
+ newlyDegraded.push(agent.agentSessionId);
109
+ }
110
+ return newlyDegraded;
111
+ }
112
+ clone(agent) {
113
+ return { ...agent };
114
+ }
115
+ }
@@ -0,0 +1,212 @@
1
+ const SNAPSHOT_STATUSES = [
2
+ "queued",
3
+ "dispatched",
4
+ "running",
5
+ "succeeded",
6
+ "failed",
7
+ "timed_out",
8
+ "cancelled",
9
+ ];
10
+ export class BackendQueue {
11
+ static DEFAULT_TERMINAL_HISTORY_LIMIT = 500;
12
+ requests = new Map();
13
+ completions = new Map();
14
+ waiters = new Map();
15
+ cancellations = new Map();
16
+ terminalOrder = [];
17
+ enqueue(input) {
18
+ if (this.requests.has(input.requestId)) {
19
+ throw new Error(`Duplicate requestId: ${input.requestId}`);
20
+ }
21
+ const request = this.cloneRequest({ ...input, status: "queued" });
22
+ this.requests.set(request.requestId, request);
23
+ return this.cloneRequest(request);
24
+ }
25
+ get(requestId) {
26
+ const request = this.requests.get(requestId);
27
+ return request ? this.cloneRequest(request) : undefined;
28
+ }
29
+ claimNext(agentSessionId, claimedAt) {
30
+ let next;
31
+ const busyNotebooks = new Set();
32
+ for (const request of this.requests.values()) {
33
+ if ((request.status === "running" || request.status === "dispatched") && request.targetNotebookId) {
34
+ busyNotebooks.add(request.targetNotebookId);
35
+ }
36
+ }
37
+ for (const request of this.requests.values()) {
38
+ if (request.status !== "queued" || request.agentSessionId !== agentSessionId)
39
+ continue;
40
+ if (busyNotebooks.has(request.targetNotebookId))
41
+ continue;
42
+ if (!next || request.createdAt < next.createdAt) {
43
+ next = request;
44
+ }
45
+ }
46
+ if (!next)
47
+ return undefined;
48
+ const updated = this.cloneRequest({ ...next, status: "running", claimedAt });
49
+ this.requests.set(updated.requestId, updated);
50
+ return this.cloneRequest(updated);
51
+ }
52
+ waitForResult(requestId) {
53
+ const request = this.requests.get(requestId);
54
+ if (!request) {
55
+ return Promise.reject(new Error("REQUEST_NOT_FOUND"));
56
+ }
57
+ const completion = this.completions.get(requestId);
58
+ if (completion) {
59
+ return completion.status === "succeeded" ? Promise.resolve(this.cloneValue(completion.result)) : Promise.reject(completion.error);
60
+ }
61
+ if (request.status === "succeeded") {
62
+ return Promise.resolve(undefined);
63
+ }
64
+ if (request.status === "failed") {
65
+ return Promise.reject(new Error("REQUEST_FAILED"));
66
+ }
67
+ if (request.status === "timed_out") {
68
+ return Promise.reject(new Error("REQUEST_TIMED_OUT"));
69
+ }
70
+ if (request.status === "cancelled") {
71
+ return Promise.reject(new Error("REQUEST_CANCELLED"));
72
+ }
73
+ return new Promise((resolve, reject) => {
74
+ const waiters = this.waiters.get(requestId) ?? [];
75
+ waiters.push({ resolve, reject });
76
+ this.waiters.set(requestId, waiters);
77
+ });
78
+ }
79
+ markTimedOut(now) {
80
+ const timedOut = [];
81
+ for (const request of this.requests.values()) {
82
+ if (request.status !== "queued" && request.status !== "running")
83
+ continue;
84
+ if (now - request.createdAt < request.timeoutMs)
85
+ continue;
86
+ const updated = this.cloneRequest({ ...request, status: "timed_out" });
87
+ this.requests.set(request.requestId, updated);
88
+ this.completions.set(request.requestId, { status: "timed_out", error: new Error("REQUEST_TIMED_OUT") });
89
+ this.settleWaiters(request.requestId, undefined, new Error("REQUEST_TIMED_OUT"));
90
+ this.recordTerminal(request.requestId);
91
+ timedOut.push(request.requestId);
92
+ }
93
+ return timedOut;
94
+ }
95
+ resolve(requestId, _payload, resolvedAt) {
96
+ const request = this.requests.get(requestId);
97
+ if (!request)
98
+ return { accepted: false, late: false };
99
+ if (request.status === "timed_out")
100
+ return { accepted: false, late: true };
101
+ if (request.status === "cancelled")
102
+ return { accepted: false, late: true };
103
+ if (request.status !== "queued" && request.status !== "running" && request.status !== "dispatched") {
104
+ return { accepted: false, late: false };
105
+ }
106
+ const updated = this.cloneRequest({ ...request, status: "succeeded", claimedAt: request.claimedAt ?? resolvedAt });
107
+ this.requests.set(requestId, updated);
108
+ this.completions.set(requestId, { status: "succeeded", result: this.cloneValue(_payload) });
109
+ this.settleWaiters(requestId, this.cloneValue(_payload), undefined);
110
+ this.recordTerminal(requestId);
111
+ return { accepted: true, late: false };
112
+ }
113
+ fail(requestId, error) {
114
+ const request = this.requests.get(requestId);
115
+ if (!request)
116
+ return false;
117
+ if (request.status !== "queued" && request.status !== "running" && request.status !== "dispatched") {
118
+ return false;
119
+ }
120
+ const rejection = this.toError(error, "REQUEST_FAILED");
121
+ this.requests.set(requestId, this.cloneRequest({ ...request, status: "failed" }));
122
+ this.completions.set(requestId, { status: "failed", error: rejection });
123
+ this.settleWaiters(requestId, undefined, rejection);
124
+ this.recordTerminal(requestId);
125
+ return true;
126
+ }
127
+ cancel(requestId, reason, cancelledAt) {
128
+ const request = this.requests.get(requestId);
129
+ if (!request)
130
+ return false;
131
+ if (request.status !== "queued" && request.status !== "running")
132
+ return false;
133
+ this.requests.set(requestId, this.cloneRequest({ ...request, status: "cancelled", claimedAt: request.claimedAt ?? cancelledAt }));
134
+ const rejection = new Error(reason);
135
+ this.completions.set(requestId, { status: "cancelled", error: rejection });
136
+ this.settleWaiters(requestId, undefined, rejection);
137
+ this.recordTerminal(requestId);
138
+ const agentSessionId = request.agentSessionId;
139
+ if (!agentSessionId)
140
+ return true;
141
+ const notices = this.cancellations.get(agentSessionId) ?? [];
142
+ notices.push({ requestId, reason });
143
+ this.cancellations.set(agentSessionId, notices);
144
+ return true;
145
+ }
146
+ cancellationsForAgent(agentSessionId) {
147
+ const notices = this.cancellations.get(agentSessionId) ?? [];
148
+ this.cancellations.set(agentSessionId, []);
149
+ return notices.map((notice) => ({ ...notice }));
150
+ }
151
+ snapshot() {
152
+ const snapshot = Object.fromEntries(SNAPSHOT_STATUSES.map((status) => [
153
+ status,
154
+ [...this.requests.values()].filter((request) => request.status === status).map((request) => this.cloneRequest(request)),
155
+ ]));
156
+ return snapshot;
157
+ }
158
+ cloneRequest(request) {
159
+ return {
160
+ ...request,
161
+ arguments: this.cloneValue(request.arguments),
162
+ };
163
+ }
164
+ cloneValue(value) {
165
+ if (value === undefined)
166
+ return value;
167
+ return typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
168
+ }
169
+ settleWaiters(requestId, value, error) {
170
+ const waiters = this.waiters.get(requestId);
171
+ if (!waiters || waiters.length === 0)
172
+ return;
173
+ this.waiters.delete(requestId);
174
+ for (const waiter of waiters) {
175
+ if (error) {
176
+ waiter.reject(error);
177
+ }
178
+ else {
179
+ waiter.resolve(this.cloneValue(value));
180
+ }
181
+ }
182
+ }
183
+ toError(value, fallbackMessage) {
184
+ if (value instanceof Error)
185
+ return value;
186
+ if (typeof value === "string" && value.trim())
187
+ return new Error(value);
188
+ if (typeof value === "object" && value !== null) {
189
+ const record = value;
190
+ const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : undefined;
191
+ const message = typeof record.message === "string" && record.message.trim() ? record.message.trim() : undefined;
192
+ if (code && message)
193
+ return new Error(`${code}: ${message}`);
194
+ if (code)
195
+ return new Error(code);
196
+ if (message)
197
+ return new Error(message);
198
+ }
199
+ return new Error(fallbackMessage);
200
+ }
201
+ recordTerminal(requestId) {
202
+ this.terminalOrder.push(requestId);
203
+ while (this.terminalOrder.length > BackendQueue.DEFAULT_TERMINAL_HISTORY_LIMIT) {
204
+ const evictedRequestId = this.terminalOrder.shift();
205
+ if (!evictedRequestId)
206
+ continue;
207
+ this.requests.delete(evictedRequestId);
208
+ this.completions.delete(evictedRequestId);
209
+ this.waiters.delete(evictedRequestId);
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,99 @@
1
+ import { AgentRegistry } from "./agentRegistry.js";
2
+ import { BackendQueue } from "./backendQueue.js";
3
+ import { NotebookRegistry } from "./notebookRegistry.js";
4
+ import { DEFAULT_TIMEOUTS_MS } from "./protocol.js";
5
+ export class BackendState {
6
+ agents;
7
+ notebooks;
8
+ queue = new BackendQueue();
9
+ activeNotebookId;
10
+ activeNotebookByClientSession = new Map();
11
+ constructor(createNotebookId) {
12
+ this.notebooks = new NotebookRegistry(createNotebookId);
13
+ this.agents = new AgentRegistry();
14
+ }
15
+ setActiveNotebook(notebookId, clientSessionId) {
16
+ if (clientSessionId) {
17
+ this.activeNotebookByClientSession.set(clientSessionId, notebookId);
18
+ }
19
+ else {
20
+ this.activeNotebookId = notebookId;
21
+ }
22
+ }
23
+ sweepLiveness(now = Date.now()) {
24
+ const degradedAgents = this.agents.markDegradedOlderThan(now, DEFAULT_TIMEOUTS_MS.agentHeartbeatDegradedMs);
25
+ for (const agentSessionId of degradedAgents) {
26
+ this.notebooks.markDegradedByAgent(agentSessionId, now);
27
+ }
28
+ const offlineAgents = this.agents.markOfflineOlderThan(now, DEFAULT_TIMEOUTS_MS.agentHeartbeatOfflineMs);
29
+ for (const agentSessionId of offlineAgents) {
30
+ this.notebooks.markStaleByAgent(agentSessionId, now);
31
+ }
32
+ this.clearInactiveActiveNotebook();
33
+ return {
34
+ offlineAgents,
35
+ staleNotebooks: this.notebooks.listAll().filter((record) => record.stale).map((record) => record.notebookId),
36
+ };
37
+ }
38
+ requireLiveAgent() {
39
+ return this.agents.hasLiveAgent() ? { ok: true } : { ok: false, error: "NO_LIVE_AGENT" };
40
+ }
41
+ resolveNotebook(selector, clientSessionId) {
42
+ if (selector.notebookId !== undefined) {
43
+ const notebookId = selector.notebookId.trim();
44
+ if (!notebookId)
45
+ return { ok: false, error: "NOTEBOOK_NOT_FOUND" };
46
+ const record = this.notebooks.get(notebookId);
47
+ if (!record)
48
+ return { ok: false, error: "NOTEBOOK_NOT_FOUND" };
49
+ if (record.closed)
50
+ return { ok: false, error: "NOTEBOOK_CLOSED" };
51
+ if (record.stale)
52
+ return { ok: false, error: "NOTEBOOK_STALE" };
53
+ return { ok: true, record };
54
+ }
55
+ if (selector.displayName !== undefined) {
56
+ const displayName = selector.displayName.trim();
57
+ if (!displayName)
58
+ return { ok: false, error: "NOTEBOOK_NOT_FOUND" };
59
+ const lookup = this.notebooks.findByDisplayName(displayName);
60
+ if (lookup.ok)
61
+ return { ok: true, record: lookup.record };
62
+ return { ok: false, error: lookup.error, candidates: lookup.candidates };
63
+ }
64
+ if (clientSessionId) {
65
+ const clientActiveNotebookId = this.activeNotebookByClientSession.get(clientSessionId);
66
+ if (clientActiveNotebookId) {
67
+ return this.resolveNotebook({ notebookId: clientActiveNotebookId });
68
+ }
69
+ }
70
+ if (this.activeNotebookId) {
71
+ return this.resolveNotebook({ notebookId: this.activeNotebookId });
72
+ }
73
+ return { ok: false, error: "NOTEBOOK_NOT_FOUND" };
74
+ }
75
+ closeNotebook(notebookId, closedAt) {
76
+ const notebook = this.notebooks.get(notebookId);
77
+ if (!notebook)
78
+ return;
79
+ this.notebooks.markClosed(notebookId, closedAt);
80
+ this.clearInactiveActiveNotebook();
81
+ if (!this.notebooks.hasLiveForAgent(notebook.agentSessionId)) {
82
+ this.agents.retire(notebook.agentSessionId, closedAt);
83
+ }
84
+ }
85
+ clearInactiveActiveNotebook() {
86
+ if (this.activeNotebookId) {
87
+ const active = this.notebooks.get(this.activeNotebookId);
88
+ if (!active || active.closed || active.stale) {
89
+ this.activeNotebookId = undefined;
90
+ }
91
+ }
92
+ for (const [clientSessionId, notebookId] of this.activeNotebookByClientSession) {
93
+ const record = this.notebooks.get(notebookId);
94
+ if (!record || record.closed || record.stale) {
95
+ this.activeNotebookByClientSession.delete(clientSessionId);
96
+ }
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,136 @@
1
+ import { canonicalizeNotebookPath } from "./protocol.js";
2
+ export class NotebookRegistry {
3
+ createNotebookId;
4
+ records = new Map();
5
+ constructor(createNotebookId) {
6
+ this.createNotebookId = createNotebookId;
7
+ }
8
+ upsertHeartbeat(input) {
9
+ const existingByFrontend = this.findByFrontend(input.agentSessionId, input.frontendObjectKey);
10
+ const normalizedPath = this.canonicalizeFirstPath(input.savedPath, input.notebookPath, input.platform);
11
+ const existingByPath = normalizedPath ? this.findByNormalizedPath(normalizedPath) : undefined;
12
+ const existing = existingByFrontend ?? existingByPath;
13
+ if (existingByFrontend && existingByPath && existingByFrontend.notebookId !== existingByPath.notebookId) {
14
+ this.markClosed(existingByPath.notebookId, input.seenAt);
15
+ }
16
+ const notebookId = existing?.notebookId ?? this.createNotebookId();
17
+ const record = {
18
+ ...input,
19
+ permissions: this.clonePermissions(input.permissions),
20
+ notebookId,
21
+ normalizedPath,
22
+ createdAt: existing?.createdAt ?? input.seenAt,
23
+ lastSeenAt: input.seenAt,
24
+ closed: false,
25
+ degradedAt: undefined,
26
+ degraded: false,
27
+ offlineAt: undefined,
28
+ stale: false,
29
+ status: "live",
30
+ };
31
+ this.records.set(notebookId, record);
32
+ return this.cloneRecord(record);
33
+ }
34
+ get(notebookId) {
35
+ const record = this.records.get(notebookId);
36
+ return record ? this.cloneRecord(record) : undefined;
37
+ }
38
+ listLive() {
39
+ return this.listAll().filter((record) => !record.closed && !record.stale).map((record) => this.cloneRecord(record));
40
+ }
41
+ listAll() {
42
+ return [...this.records.values()].map((record) => this.cloneRecord(record));
43
+ }
44
+ hasLiveForAgent(agentSessionId) {
45
+ for (const record of this.records.values()) {
46
+ if (record.agentSessionId !== agentSessionId)
47
+ continue;
48
+ if (record.closed || record.stale)
49
+ continue;
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ findByDisplayName(displayName) {
55
+ const matches = this.listLive().filter((record) => record.displayName === displayName || record.windowTitle === displayName);
56
+ if (matches.length === 1) {
57
+ return { ok: true, record: this.cloneRecord(matches[0]) };
58
+ }
59
+ if (matches.length > 1) {
60
+ return { ok: false, error: "AMBIGUOUS_NOTEBOOK_NAME", candidates: matches };
61
+ }
62
+ return { ok: false, error: "NOTEBOOK_NOT_FOUND", candidates: [] };
63
+ }
64
+ markClosed(notebookId, closedAt) {
65
+ const record = this.records.get(notebookId);
66
+ if (!record)
67
+ return;
68
+ this.records.set(notebookId, {
69
+ ...record,
70
+ closed: true,
71
+ degraded: false,
72
+ degradedAt: undefined,
73
+ offlineAt: undefined,
74
+ stale: false,
75
+ status: "closed",
76
+ lastSeenAt: closedAt,
77
+ });
78
+ }
79
+ markDegradedByAgent(agentSessionId, degradedAt) {
80
+ for (const record of this.records.values()) {
81
+ if (record.agentSessionId !== agentSessionId || record.closed || record.stale || record.degraded)
82
+ continue;
83
+ this.records.set(record.notebookId, {
84
+ ...record,
85
+ degraded: true,
86
+ degradedAt,
87
+ offlineAt: undefined,
88
+ status: "degraded",
89
+ });
90
+ }
91
+ }
92
+ markStaleByAgent(agentSessionId, staleAt) {
93
+ for (const record of this.records.values()) {
94
+ if (record.agentSessionId !== agentSessionId || record.closed || record.stale)
95
+ continue;
96
+ this.records.set(record.notebookId, {
97
+ ...record,
98
+ degraded: false,
99
+ degradedAt: undefined,
100
+ offlineAt: staleAt,
101
+ stale: true,
102
+ status: "offline",
103
+ });
104
+ }
105
+ }
106
+ findByFrontend(agentSessionId, frontendObjectKey) {
107
+ for (const record of this.records.values()) {
108
+ if (record.closed)
109
+ continue;
110
+ if (record.agentSessionId === agentSessionId && record.frontendObjectKey === frontendObjectKey) {
111
+ return record;
112
+ }
113
+ }
114
+ return undefined;
115
+ }
116
+ findByNormalizedPath(normalizedPath) {
117
+ for (const record of this.records.values()) {
118
+ if (!record.closed && record.normalizedPath === normalizedPath) {
119
+ return record;
120
+ }
121
+ }
122
+ return undefined;
123
+ }
124
+ canonicalizeFirstPath(savedPath, notebookPath, platform) {
125
+ return canonicalizeNotebookPath(savedPath, platform) ?? canonicalizeNotebookPath(notebookPath, platform);
126
+ }
127
+ cloneRecord(record) {
128
+ return {
129
+ ...record,
130
+ permissions: this.clonePermissions(record.permissions),
131
+ };
132
+ }
133
+ clonePermissions(permissions) {
134
+ return { ...permissions };
135
+ }
136
+ }
@@ -0,0 +1,32 @@
1
+ import path from "node:path";
2
+ export const DEFAULT_TIMEOUTS_MS = {
3
+ status: 5000,
4
+ listNotebooks: 5000,
5
+ listCells: 10_000,
6
+ readCell: 10_000,
7
+ mutation: 10_000,
8
+ insertCell: 60_000,
9
+ runCell: 120_000,
10
+ symbolLookup: 30_000,
11
+ agentHeartbeatDegradedMs: 10_000,
12
+ agentHeartbeatOfflineMs: 30_000,
13
+ };
14
+ function looksLikeWindowsPath(input) {
15
+ return /^[A-Za-z]:[\\/]/.test(input) || /^\\\\/.test(input);
16
+ }
17
+ function usesWindowsPathSemantics(platform, input) {
18
+ if (platform === "Windows")
19
+ return true;
20
+ if (platform !== undefined)
21
+ return false;
22
+ return process.platform === "win32" || looksLikeWindowsPath(input);
23
+ }
24
+ export function canonicalizeNotebookPath(input, platform) {
25
+ const trimmed = input?.trim();
26
+ if (!trimmed)
27
+ return undefined;
28
+ if (usesWindowsPathSemantics(platform, trimmed)) {
29
+ return path.win32.normalize(trimmed).toLowerCase();
30
+ }
31
+ return path.posix.normalize(trimmed);
32
+ }