aicodeman 1.29.1 → 1.30.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/reboot-restore.d.ts +146 -0
- package/dist/reboot-restore.d.ts.map +1 -0
- package/dist/reboot-restore.js +205 -0
- package/dist/reboot-restore.js.map +1 -0
- package/dist/session-env-clamp.d.ts +80 -0
- package/dist/session-env-clamp.d.ts.map +1 -0
- package/dist/session-env-clamp.js +96 -0
- package/dist/session-env-clamp.js.map +1 -0
- package/dist/session.d.ts +9 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +12 -0
- package/dist/session.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +31 -0
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.556be563.js → app.6d2dc4e8.js} +2 -2
- package/dist/web/public/app.6d2dc4e8.js.br +0 -0
- package/dist/web/public/app.6d2dc4e8.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/constants.258b140f.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/i18n.5f897ed5.js.gz +0 -0
- package/dist/web/public/image-input.cd4b97c4.js.gz +0 -0
- package/dist/web/public/index.html +25 -6
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.2cf04f17.js.gz +0 -0
- package/dist/web/public/mobile-handlers.6f354a87.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/{mobile.e9d0b53e.css → mobile.6caaa28a.css} +1 -1
- package/dist/web/public/mobile.6caaa28a.css.br +0 -0
- package/dist/web/public/mobile.6caaa28a.css.gz +0 -0
- package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/panels-ui.5b07ad14.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js +142 -0
- package/dist/web/public/reboot-restore-ui.js.br +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.42b81477.js.gz +0 -0
- package/dist/web/public/settings-ui.58f1d756.js.gz +0 -0
- package/dist/web/public/styles.e6edfb0b.css +1 -0
- package/dist/web/public/styles.e6edfb0b.css.br +0 -0
- package/dist/web/public/styles.e6edfb0b.css.gz +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js +1 -0
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
- package/dist/web/public/{terminal-ui.38c49244.js → terminal-ui.4f8d5820.js} +2 -2
- package/dist/web/public/terminal-ui.4f8d5820.js.br +0 -0
- package/dist/web/public/terminal-ui.4f8d5820.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/reboot-restore-registry.d.ts +107 -0
- package/dist/web/reboot-restore-registry.d.ts.map +1 -0
- package/dist/web/reboot-restore-registry.js +189 -0
- package/dist/web/reboot-restore-registry.js.map +1 -0
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.d.ts +30 -0
- package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -0
- package/dist/web/routes/reboot-restore-routes.js +273 -0
- package/dist/web/routes/reboot-restore-routes.js.map +1 -0
- package/dist/web/routes/session-routes.d.ts +1 -18
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +1 -65
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +11 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +13 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +74 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +238 -2
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.556be563.js.br +0 -0
- package/dist/web/public/app.556be563.js.gz +0 -0
- package/dist/web/public/mobile.e9d0b53e.css.br +0 -0
- package/dist/web/public/mobile.e9d0b53e.css.gz +0 -0
- package/dist/web/public/styles.6add175d.css +0 -1
- package/dist/web/public/styles.6add175d.css.br +0 -0
- package/dist/web/public/styles.6add175d.css.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js +0 -1
- package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js.gz +0 -0
- package/dist/web/public/terminal-ui.38c49244.js.br +0 -0
- package/dist/web/public/terminal-ui.38c49244.js.gz +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Reboot-restore routes: offer back the sessions a host reboot destroyed.
|
|
3
|
+
*
|
|
4
|
+
* The boot pass leaves a plan in `web/reboot-restore-registry` when the machine
|
|
5
|
+
* plausibly rebooted. The board reads it, shows a banner, and the user decides:
|
|
6
|
+
* - `GET /api/reboot-restore`: what is on offer, ownership-scoped
|
|
7
|
+
* - `POST /api/reboot-restore/restore`: rebuild some or all of it
|
|
8
|
+
* - `POST /api/reboot-restore/dismiss`: drop the offer
|
|
9
|
+
*
|
|
10
|
+
* A click, not the heuristic, is what creates panes. The heuristic only decides
|
|
11
|
+
* whether the banner appears, so a wrong yes costs a line of text the user
|
|
12
|
+
* dismisses rather than N CLI processes nobody asked for.
|
|
13
|
+
*
|
|
14
|
+
* Rebuilding is take-then-build: entries leave the plan synchronously at the top
|
|
15
|
+
* of the route, before the first `await`, and the whole route is single-flighted,
|
|
16
|
+
* so a double-click or two devices cannot put two panes on one conversation.
|
|
17
|
+
* Three things are re-checked at click time rather than trusted from boot: the
|
|
18
|
+
* owner's privilege grant, the workspace still being on disk, and the
|
|
19
|
+
* conversation not already being live because the user resumed it by hand.
|
|
20
|
+
*
|
|
21
|
+
* A rebuilt session comes back attached, idle and disarmed. Respawn controllers
|
|
22
|
+
* and Ralph loops are deliberately not re-armed, and its terminal scrollback is
|
|
23
|
+
* gone, because the pane is new. The banner says so.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync } from 'node:fs';
|
|
26
|
+
import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
|
|
27
|
+
import { RebootRestoreRequestSchema } from '../schemas.js';
|
|
28
|
+
import { parseBody, getAuthUser, canAccessOwned, ownerFor, isWorkingDirAllowedForUsername, sessionCapacityMessage, } from '../route-helpers.js';
|
|
29
|
+
import { rebootRestoreRegistry } from '../reboot-restore-registry.js';
|
|
30
|
+
import { rejectAlreadyLive } from '../../reboot-restore.js';
|
|
31
|
+
import { clampEnvOverridesForOwner } from '../../session-env-clamp.js';
|
|
32
|
+
import { Session } from '../../session.js';
|
|
33
|
+
import { resolveClaudeModeForUsername } from '../../user-store.js';
|
|
34
|
+
import { getCli } from '../../config/cli-registry/registry.js';
|
|
35
|
+
import { applyWorkspaceHooks, seedAgentSessionPreamble } from '../../hooks-config.js';
|
|
36
|
+
import { getLifecycleLog } from '../../session-lifecycle-log.js';
|
|
37
|
+
import { STATS_COLLECTION_INTERVAL_MS } from '../../config/server-timing.js';
|
|
38
|
+
import { SseEvent } from '../sse-events.js';
|
|
39
|
+
/** The banner's view of one restorable session. The record itself never leaves the server. */
|
|
40
|
+
function toBannerItem(entry) {
|
|
41
|
+
return {
|
|
42
|
+
id: entry.sessionId,
|
|
43
|
+
name: entry.name,
|
|
44
|
+
workingDir: entry.workingDir,
|
|
45
|
+
mode: entry.mode,
|
|
46
|
+
owner: entry.owner,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function registerRebootRestoreRoutes(app, ctx) {
|
|
50
|
+
const accessorFor = (req) => {
|
|
51
|
+
const user = getAuthUser(req);
|
|
52
|
+
return (owner) => canAccessOwned(user, owner);
|
|
53
|
+
};
|
|
54
|
+
// ========== What is on offer ==========
|
|
55
|
+
app.get('/api/reboot-restore', async (req) => {
|
|
56
|
+
const entries = rebootRestoreRegistry.list(accessorFor(req));
|
|
57
|
+
return {
|
|
58
|
+
sessions: entries.map(toBannerItem),
|
|
59
|
+
// Said plainly here so the banner never implies a full restore: the pane is
|
|
60
|
+
// new, so the conversation continues and the terminal history does not.
|
|
61
|
+
scrollbackRestored: false,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
// ========== Spend it ==========
|
|
65
|
+
app.post('/api/reboot-restore/restore', async (req, reply) => {
|
|
66
|
+
const body = parseBody(RebootRestoreRequestSchema, req.body, 'Invalid reboot restore request');
|
|
67
|
+
const canAccess = accessorFor(req);
|
|
68
|
+
const owner = ownerFor(req);
|
|
69
|
+
// Take BEFORE the first await: a second click must find nothing to spend.
|
|
70
|
+
// The flight is per owner, because `take()` already guarantees two callers
|
|
71
|
+
// never receive the same entry, so one user's restore need not block another's.
|
|
72
|
+
if (!rebootRestoreRegistry.beginSpending(owner)) {
|
|
73
|
+
return reply.code(409).send(createErrorResponse(ApiErrorCode.CONFLICT, 'A reboot restore is already running'));
|
|
74
|
+
}
|
|
75
|
+
const taken = rebootRestoreRegistry.take(canAccess, body.sessionIds, owner);
|
|
76
|
+
// Entries nothing built a pane for, returned to the plan on every exit path
|
|
77
|
+
// including a throw. Without this a failure between here and the loop would
|
|
78
|
+
// spend the offer and rebuild nothing, and the plan cannot be rebuilt.
|
|
79
|
+
const unspent = new Set(taken);
|
|
80
|
+
try {
|
|
81
|
+
if (taken.length === 0)
|
|
82
|
+
return { restored: [], skipped: [] };
|
|
83
|
+
// The plan was built at boot and the board has moved on since. A conversation
|
|
84
|
+
// the user resumed by hand from the Resume list is already on screen, and a
|
|
85
|
+
// second pane on it would fight the first for the same transcript. This one
|
|
86
|
+
// is never re-offered: unlike a missing workspace, it cannot stop being true.
|
|
87
|
+
// Read fresh each time rather than snapshotted once: the loop below awaits a
|
|
88
|
+
// real `startInteractive()` per entry, so by the tenth entry a snapshot taken
|
|
89
|
+
// here is tens of seconds old, and a conversation the user resumed by hand in
|
|
90
|
+
// that window would be invisible to it.
|
|
91
|
+
const liveSessionIds = () => new Set(ctx.sessions.keys());
|
|
92
|
+
const liveConversationIds = () => new Set([...ctx.sessions.values()].map((session) => session.claudeSessionId).filter((id) => !!id));
|
|
93
|
+
const { restore, skipped } = rejectAlreadyLive(taken, liveSessionIds(), liveConversationIds());
|
|
94
|
+
for (const entry of taken) {
|
|
95
|
+
if (skipped.some((s) => s.sessionId === entry.sessionId))
|
|
96
|
+
unspent.delete(entry);
|
|
97
|
+
}
|
|
98
|
+
const restored = [];
|
|
99
|
+
const failures = [...skipped];
|
|
100
|
+
const workspaceHooksEnabled = await ctx.getWorkspaceHooksEnabled();
|
|
101
|
+
for (const entry of restore) {
|
|
102
|
+
// The already-live check, re-run against the board as it is NOW. The pass
|
|
103
|
+
// above decided the batch; this catches a conversation that went live while
|
|
104
|
+
// an earlier entry in this same batch was starting. Spent rather than
|
|
105
|
+
// returned to the plan, for the same reason as the batch pass: unlike a
|
|
106
|
+
// missing workspace or a withdrawn grant, an open conversation is not a
|
|
107
|
+
// condition that stops being true.
|
|
108
|
+
const [lateLive] = rejectAlreadyLive([entry], liveSessionIds(), liveConversationIds()).skipped;
|
|
109
|
+
if (lateLive) {
|
|
110
|
+
failures.push(lateLive);
|
|
111
|
+
unspent.delete(entry);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Capacity is re-checked per iteration, because this loop is itself
|
|
115
|
+
// creating the sessions it counts. The offer can be a day old, so the
|
|
116
|
+
// board may be fuller now than the plan assumed.
|
|
117
|
+
const capMsg = sessionCapacityMessage(ctx.sessions, entry.owner);
|
|
118
|
+
if (capMsg) {
|
|
119
|
+
failures.push({ sessionId: entry.sessionId, reason: 'capacity-reached' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// A repo can be deleted between the boot that planned this and the click.
|
|
123
|
+
if (!existsSync(entry.workingDir)) {
|
|
124
|
+
failures.push({ sessionId: entry.sessionId, reason: 'workspace-missing' });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Multi-user workspace separation: the create route confines a non-admin's
|
|
128
|
+
// workingDir to their own case space, and a grant can be withdrawn between
|
|
129
|
+
// the session's creation and this restore, so the confinement is re-run
|
|
130
|
+
// rather than inherited from the record. Keyed on the OWNER, not on the
|
|
131
|
+
// caller: an admin spending another user's entry must be held to that
|
|
132
|
+
// user's confinement, and `isWorkingDirAllowed` would wave an admin
|
|
133
|
+
// through. The same reason the two grant re-checks below read
|
|
134
|
+
// `saved.owner`.
|
|
135
|
+
if (!(await isWorkingDirAllowedForUsername(entry.owner, entry.workingDir))) {
|
|
136
|
+
// Left on offer: a withdrawn grant can be restored, unlike an already-open
|
|
137
|
+
// conversation, so this is not the permanent kind of refusal.
|
|
138
|
+
failures.push({ sessionId: entry.sessionId, reason: 'workspace-forbidden' });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const saved = entry.state;
|
|
143
|
+
const claudeModeConfig = await ctx.getClaudeModeConfig();
|
|
144
|
+
const session = new Session({
|
|
145
|
+
// The old id is reused on purpose: a pinned record, subagent parents,
|
|
146
|
+
// window states and the lifecycle log all key off it, and the unpinned
|
|
147
|
+
// record is gone, so there is nothing to collide with.
|
|
148
|
+
id: saved.id,
|
|
149
|
+
workingDir: saved.workingDir,
|
|
150
|
+
mode: saved.mode,
|
|
151
|
+
name: saved.name,
|
|
152
|
+
// Without this the constructor re-infers ownership from the name, so a
|
|
153
|
+
// session the user renamed by hand to something shaped like `w<n>-<case>`
|
|
154
|
+
// comes back as `placeholder` and auto-naming overwrites their name on
|
|
155
|
+
// the next prompt. The route persists below, so the loss would go to
|
|
156
|
+
// disk. `restoreMuxSessions()` passes it for the same reason.
|
|
157
|
+
nameSource: saved.nameSource,
|
|
158
|
+
createdAt: saved.createdAt,
|
|
159
|
+
mux: ctx.mux,
|
|
160
|
+
useMux: true,
|
|
161
|
+
// No `muxSession`: the reboot took the pane with it, so `startInteractive()`
|
|
162
|
+
// takes its create branch and makes a fresh one.
|
|
163
|
+
claudeMode: await resolveClaudeModeForUsername(claudeModeConfig.claudeMode, saved.owner),
|
|
164
|
+
allowedTools: claudeModeConfig.allowedTools,
|
|
165
|
+
resumeSessionId: entry.resumeConversationId,
|
|
166
|
+
// Re-resolved against the owner's CURRENT grant, never replayed from the
|
|
167
|
+
// record: a grant held when the record was written may be gone now.
|
|
168
|
+
envOverrides: await clampEnvOverridesForOwner(saved.owner, saved.__envOverrides),
|
|
169
|
+
effort: saved.effort,
|
|
170
|
+
attachmentHistory: saved.__attachmentHistory ??
|
|
171
|
+
saved.attachmentHistory,
|
|
172
|
+
lastSubmitAt: saved.lastSubmitAt,
|
|
173
|
+
claudeSessionChain: saved.claudeSessionChain,
|
|
174
|
+
lastActivityAt: saved.lastActivityAt,
|
|
175
|
+
owner: saved.owner,
|
|
176
|
+
parentSessionId: saved.parentSessionId,
|
|
177
|
+
});
|
|
178
|
+
await ctx.addSession(session);
|
|
179
|
+
// Before the listeners, because setupSessionListeners() reads the
|
|
180
|
+
// image-watcher flag this phase restores; before the spawn, because the
|
|
181
|
+
// custom-model environment and the nice priority shape the process.
|
|
182
|
+
await ctx.reapplyPersistedSessionState(session, saved, 'before-spawn');
|
|
183
|
+
await ctx.setupSessionListeners(session);
|
|
184
|
+
await session.startInteractive();
|
|
185
|
+
// The session's own history, applied only once the pane exists: on a
|
|
186
|
+
// failed start these totals would belong to a session that never ran.
|
|
187
|
+
// Both halves precede the route's OWN persist, which matters because a
|
|
188
|
+
// constructed session carries none of this and `toState()` is written
|
|
189
|
+
// wholesale, so persisting first would replace the fuller record with
|
|
190
|
+
// the reduced one and drop the pin that keeps it from being pruned. A
|
|
191
|
+
// listener-driven persist can still land inside the debounce window
|
|
192
|
+
// while the pane starts; the write below repairs the record.
|
|
193
|
+
// `rearmAutoResumeSchedule: false`: the saved stamp predates the reboot and
|
|
194
|
+
// the pane is new, so honouring it would have every restored session type
|
|
195
|
+
// `continue` into itself about a minute after one click. Auto-resume stays
|
|
196
|
+
// enabled and re-arms on the next real limit message. This is also what the
|
|
197
|
+
// module header promises ("comes back attached, idle and disarmed").
|
|
198
|
+
await ctx.reapplyPersistedSessionState(session, saved, 'after-spawn', {
|
|
199
|
+
rearmAutoResumeSchedule: false,
|
|
200
|
+
});
|
|
201
|
+
ctx.persistSessionState(session);
|
|
202
|
+
// A session without its workspace hooks goes silently blind: no stop or
|
|
203
|
+
// idle events for respawn, no Approvals Inbox item, no red tab on a
|
|
204
|
+
// blocking dialog. The boot-time sweep finished hours ago, so the click
|
|
205
|
+
// path installs them itself. `hooks: 'always'` is the capability that says
|
|
206
|
+
// this CLI installs Codeman's hooks into the workspace.
|
|
207
|
+
if (workspaceHooksEnabled && getCli(session.mode)?.capabilities.hooks === 'always') {
|
|
208
|
+
await applyWorkspaceHooks(session.workingDir, true).catch((err) => console.warn(`[reboot-restore] hook install failed for ${session.workingDir}: ${getErrorMessage(err)}`));
|
|
209
|
+
}
|
|
210
|
+
// Both create paths seed this; without it a restored claude session's agent
|
|
211
|
+
// skill falls back to writing out the whole ~150-line §0 preamble. Remote and
|
|
212
|
+
// docker sessions never reach here (the plan rejects them as
|
|
213
|
+
// `remote-or-docker`), so the local-only condition is structural.
|
|
214
|
+
if (getCli(session.mode)?.capabilities.agentSkillInjection && (await ctx.getAgentSkillEnabled())) {
|
|
215
|
+
await seedAgentSessionPreamble(session.id).catch((err) => console.warn(`[agent-skill] preamble seed failed for ${session.id}: ${getErrorMessage(err)}`));
|
|
216
|
+
}
|
|
217
|
+
getLifecycleLog().log({ event: 'recovered', sessionId: session.id, name: session.name });
|
|
218
|
+
// Every other open tab and phone needs this; the clicking tab already has
|
|
219
|
+
// the response, and the client's handler is an idempotent upsert.
|
|
220
|
+
ctx.broadcast(SseEvent.SessionCreated, ctx.getSessionStateWithRespawn(session));
|
|
221
|
+
restored.push(toBannerItem(entry));
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
// One entry that will not start must not stop the rest of the pass, and
|
|
225
|
+
// must not leave a registered session with no pane behind it: by this
|
|
226
|
+
// point the session is in `ctx.sessions`, holds a tab-layout slot and has
|
|
227
|
+
// listeners.
|
|
228
|
+
//
|
|
229
|
+
// Reaching this is rarer than it looks, measured against a real server:
|
|
230
|
+
// the CLI resolver finds its binary by absolute path rather than through
|
|
231
|
+
// PATH, and tmux falls back to another directory rather than failing when
|
|
232
|
+
// it cannot enter the workspace, so neither of the two obvious "freshly
|
|
233
|
+
// booted machine" failures throws. What is left is the mux layer itself
|
|
234
|
+
// failing, which is why this path is defended rather than expected.
|
|
235
|
+
console.error(`[reboot-restore] failed to rebuild ${entry.sessionId}:`, err);
|
|
236
|
+
// Not cleanupSession(): that is the user-initiated delete, and it would
|
|
237
|
+
// count this session's historical tokens into the lifetime totals, demote
|
|
238
|
+
// a pinned record to `stopped` (which this pass reads as an intentional
|
|
239
|
+
// kill, making the session permanently unrestorable) and delete the
|
|
240
|
+
// workspace's `.claude-images`. This undoes only the construction.
|
|
241
|
+
await ctx
|
|
242
|
+
.discardPartiallyBuiltSession(entry.sessionId)
|
|
243
|
+
.catch((discardErr) => console.error(`[reboot-restore] discarding a failed rebuild failed: ${getErrorMessage(discardErr)}`));
|
|
244
|
+
failures.push({ sessionId: entry.sessionId, reason: 'rebuild-failed' });
|
|
245
|
+
// Left on offer: the user can put the binary back and click again.
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
unspent.delete(entry);
|
|
249
|
+
}
|
|
250
|
+
if (restored.length > 0) {
|
|
251
|
+
// A reboot leaves recovery with nothing alive to find, so its own block never
|
|
252
|
+
// started the stats collector. This clears and re-arms its interval, so it is
|
|
253
|
+
// safe to call whether or not the collector is already running.
|
|
254
|
+
ctx.mux.startStatsCollection(STATS_COLLECTION_INTERVAL_MS);
|
|
255
|
+
}
|
|
256
|
+
return { restored, skipped: failures };
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
// Anything that never became a pane goes back on offer, including after a
|
|
260
|
+
// throw, so a transient failure costs a retry rather than the whole plan.
|
|
261
|
+
// Ends the flight: entries still parked for it come back if they are in
|
|
262
|
+
// `unspent`, and a Dismiss that unparked them meanwhile wins.
|
|
263
|
+
rebootRestoreRegistry.releaseFlight(owner, [...unspent]);
|
|
264
|
+
rebootRestoreRegistry.endSpending(owner);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// ========== Drop it ==========
|
|
268
|
+
app.post('/api/reboot-restore/dismiss', async (req) => {
|
|
269
|
+
const dismissed = rebootRestoreRegistry.clear(accessorFor(req));
|
|
270
|
+
return { dismissed };
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=reboot-restore-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reboot-restore-routes.js","sourceRoot":"","sources":["../../../src/web/routes/reboot-restore-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { FastifyInstance } from 'fastify';
|
|
7
7
|
import { type SessionMode, type CodexConfig, type GeminiConfig, type AntigravityConfig, type PiConfig, type GrokConfig, type DeepSeekConfig, type OmpConfig } from '../../types.js';
|
|
8
|
+
import { clampEnvOverridesForOwner } from '../../session-env-clamp.js';
|
|
8
9
|
import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort, TabLayoutPort } from '../ports/index.js';
|
|
9
10
|
/**
|
|
10
11
|
* Strip redundant Ink spinner/status-bar redraw frames from the terminal buffer.
|
|
@@ -49,23 +50,6 @@ export declare function _clampExternalCliBypassForOwner(owner: string | undefine
|
|
|
49
50
|
grokConfig: GrokConfig | undefined;
|
|
50
51
|
deepSeekConfig: DeepSeekConfig | undefined;
|
|
51
52
|
}>;
|
|
52
|
-
/**
|
|
53
|
-
* Env-var half of the multi-user bypass clamp.
|
|
54
|
-
*
|
|
55
|
-
* `clampExternalCliBypassForOwner()` clamps the per-CLI CONFIG, and for every CLI
|
|
56
|
-
* but DeepSeek that is the whole story. Here it is not: `applyEnvOverrides()` runs
|
|
57
|
-
* AFTER `_configureCliEnv()` in tmux-manager, so an override sent on the SAME
|
|
58
|
-
* request lands last and wins, and a non-granted owner could restore
|
|
59
|
-
* `danger-full-access` on the very request the config clamp downgraded.
|
|
60
|
-
*
|
|
61
|
-
* Keys are DROPPED rather than rewritten: dropping falls through to what
|
|
62
|
-
* `_configureCliEnv()` exports, which is the clamped config and the server's own
|
|
63
|
-
* `DSH_HOME`, i.e. exactly the intended state. No-op in single-user mode and for a
|
|
64
|
-
* granted owner, like every other clamp here
|
|
65
|
-
* (`canUsernameRunPrivilegedCommands()` returns true when `!isMultiUserMode()`),
|
|
66
|
-
* and it returns the caller's own object untouched when there is nothing to strip.
|
|
67
|
-
*/
|
|
68
|
-
declare function clampEnvOverridesForOwner(owner: string | undefined, envOverrides: Record<string, string> | undefined): Promise<Record<string, string> | undefined>;
|
|
69
53
|
/** Test hook: the env-var half of the same multi-user safety gate. */
|
|
70
54
|
export declare const _clampEnvOverridesForOwner: typeof clampEnvOverridesForOwner;
|
|
71
55
|
/** Test seam: pane-liveness state is module-level, so a suite must be able to reset it. */
|
|
@@ -86,5 +70,4 @@ export declare function _paneDeathWatcherCount(): number;
|
|
|
86
70
|
*/
|
|
87
71
|
export declare function resolveOmpConfigForCreate(mode: SessionMode, workingDir: string, ompConfig: OmpConfig | undefined): OmpConfig | undefined;
|
|
88
72
|
export declare function registerSessionRoutes(app: FastifyInstance, ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort & TabLayoutPort): void;
|
|
89
|
-
export {};
|
|
90
73
|
//# sourceMappingURL=session-routes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/session-routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAC;AAS7D,OAAO,EAOL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"session-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/session-routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAC;AAS7D,OAAO,EAOL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AA6DxB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAmCvE,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AA0EhH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CA0C1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAmCvE;AAWD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,OAAO,CAiBhF;AAGD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAoED;;;;;;;GAOG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,YAAY,CAAC,EAAE,YAAY,EAC3B,iBAAiB,CAAC,EAAE,iBAAiB,EACrC,QAAQ,CAAC,EAAE,QAAQ,EACnB,UAAU,CAAC,EAAE,UAAU,EACvB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC;IACT,WAAW,EAAE,WAAW,GAAG,SAAS,CAAC;IACrC,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;IACvC,iBAAiB,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACjD,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC/B,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;IACnC,cAAc,EAAE,cAAc,GAAG,SAAS,CAAC;CAC5C,CAAC,CAiBD;AAED,sEAAsE;AACtE,eAAO,MAAM,0BAA0B,kCAA4B,CAAC;AA8LpE,2FAA2F;AAC3F,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AA2ED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,SAAS,GAAG,SAAS,GAC/B,SAAS,GAAG,SAAS,CAYvB;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAC/E,IAAI,CA00HN"}
|
|
@@ -25,6 +25,7 @@ import { clampWaitMs, MAX_BUFFER_SCAN_BYTES } from '../../config/agent-wait.js';
|
|
|
25
25
|
import { autoConfigureRalph, canAccessOwned, CASES_DIR, findPersistedSessionOrFail, findSessionOrFail, getAuthUser, isAdmin, isWorkingDirAllowed, ownerFor, parseBody, persistAndBroadcastSession, resolveAgentCaseOrigin, resolveCasesDir, resolveParentSessionId, sessionCapacityMessage, SETTINGS_PATH, validatePathWithinBase, } from '../route-helpers.js';
|
|
26
26
|
import { buildAgentCaseMarker, writeAgentCaseMarker } from '../../agent-case-marker.js';
|
|
27
27
|
import { canUsernameRunPrivilegedCommands, resolveClaudeModeForUsername } from '../../user-store.js';
|
|
28
|
+
import { clampEnvOverridesForOwner } from '../../session-env-clamp.js';
|
|
28
29
|
import { enabledClis, getCli } from '../../config/cli-registry/registry.js';
|
|
29
30
|
import { resolveCliLaunchError } from '../../utils/cli-launcher.js';
|
|
30
31
|
import { legacyConfigForMode } from '../../session-cli-registry-bridge.js';
|
|
@@ -301,71 +302,6 @@ export async function _clampExternalCliBypassForOwner(owner, codexConfig, gemini
|
|
|
301
302
|
});
|
|
302
303
|
return out;
|
|
303
304
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Env-var keys a non-granted owner must not be able to set, because each one
|
|
306
|
-
* hands back privilege the config clamp above just removed, or redirects a
|
|
307
|
-
* credential-resolution endpoint.
|
|
308
|
-
*
|
|
309
|
-
* The DeepSeek three are reachable because `DSH_*` and `DEEPSEEK_*` are
|
|
310
|
-
* allowlisted `envOverrides` prefixes (schemas.ts) — which they have to be, since
|
|
311
|
-
* that is also how a user configures the harness's non-privileged knobs.
|
|
312
|
-
*
|
|
313
|
-
* - `DSH_PERMISSION_MODE` IS the harness's permission switch. Every other CLI's
|
|
314
|
-
* bypass is a command-line FLAG, reachable only through the per-CLI config the
|
|
315
|
-
* clamp already owns; this one is an env var, so the config clamp alone is
|
|
316
|
-
* half a gate.
|
|
317
|
-
* - `DSH_HOME` points the launcher at a profile tree, and a profile's plugin code
|
|
318
|
-
* executes at BOOT, before any approval row can apply. A user who can write a
|
|
319
|
-
* workspace can put a profile in it, so this is the wider of the two.
|
|
320
|
-
* - `DEEPSEEK_BASE_URL` aims the provider endpoint, and `_configureCliEnv()`
|
|
321
|
-
* forwards the SERVER's own `DEEPSEEK_API_KEY` into every dsh pane before
|
|
322
|
-
* `applyEnvOverrides()` runs — so a non-granted owner who could set the base
|
|
323
|
-
* URL would have the operator's API key sent as a bearer credential to a host
|
|
324
|
-
* of their choosing. (`DEEPSEEK_API_KEY` itself stays overridable: supplying
|
|
325
|
-
* your OWN key removes privilege rather than granting it.)
|
|
326
|
-
* - `OMP_AUTH_BROKER_URL`/`OMP_AUTH_BROKER_TOKEN` are where omp resolves
|
|
327
|
-
* credentials from — the same shape as `DEEPSEEK_BASE_URL` above, reachable
|
|
328
|
-
* because `OMP_*` is an allowlisted prefix. Unlike DeepSeek, Codeman does not
|
|
329
|
-
* forward any operator-held key into an omp pane today (omp's provider
|
|
330
|
-
* credentials live in `~/.omp` config files, not env vars), so there is no
|
|
331
|
-
* known concrete exfiltration path yet — clamped defensively anyway, since a
|
|
332
|
-
* non-granted owner redirecting where a shared multi-tenant deployment
|
|
333
|
-
* resolves auth from is not something to allow silently (found in
|
|
334
|
-
* Ark0N/Codeman#353 review; omp's own knobs are otherwise mostly `PI_*`,
|
|
335
|
-
* already allowlisted for pi and not addressed here — see resolveOmpHome()).
|
|
336
|
-
*/
|
|
337
|
-
function ownerClampedEnvKeys() {
|
|
338
|
-
return enabledClis().flatMap((entry) => entry.capabilities.privilegedEnvKeys);
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Env-var half of the multi-user bypass clamp.
|
|
342
|
-
*
|
|
343
|
-
* `clampExternalCliBypassForOwner()` clamps the per-CLI CONFIG, and for every CLI
|
|
344
|
-
* but DeepSeek that is the whole story. Here it is not: `applyEnvOverrides()` runs
|
|
345
|
-
* AFTER `_configureCliEnv()` in tmux-manager, so an override sent on the SAME
|
|
346
|
-
* request lands last and wins, and a non-granted owner could restore
|
|
347
|
-
* `danger-full-access` on the very request the config clamp downgraded.
|
|
348
|
-
*
|
|
349
|
-
* Keys are DROPPED rather than rewritten: dropping falls through to what
|
|
350
|
-
* `_configureCliEnv()` exports, which is the clamped config and the server's own
|
|
351
|
-
* `DSH_HOME`, i.e. exactly the intended state. No-op in single-user mode and for a
|
|
352
|
-
* granted owner, like every other clamp here
|
|
353
|
-
* (`canUsernameRunPrivilegedCommands()` returns true when `!isMultiUserMode()`),
|
|
354
|
-
* and it returns the caller's own object untouched when there is nothing to strip.
|
|
355
|
-
*/
|
|
356
|
-
async function clampEnvOverridesForOwner(owner, envOverrides) {
|
|
357
|
-
if (!envOverrides)
|
|
358
|
-
return envOverrides;
|
|
359
|
-
const keys = ownerClampedEnvKeys();
|
|
360
|
-
if (!keys.some((key) => key in envOverrides))
|
|
361
|
-
return envOverrides;
|
|
362
|
-
if (await canUsernameRunPrivilegedCommands(owner))
|
|
363
|
-
return envOverrides;
|
|
364
|
-
const clamped = { ...envOverrides };
|
|
365
|
-
for (const key of keys)
|
|
366
|
-
delete clamped[key];
|
|
367
|
-
return clamped;
|
|
368
|
-
}
|
|
369
305
|
/** Test hook: the env-var half of the same multi-user safety gate. */
|
|
370
306
|
export const _clampEnvOverridesForOwner = clampEnvOverridesForOwner;
|
|
371
307
|
// ═══════════════════════════════════════════════════════════════
|