@miphamai/cli 0.85.1 → 0.85.3
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/bin/mipham.ts +42 -2
- package/package.json +1 -1
- package/skills/standard/mipham-code-setup.SKILL.md +9 -2
- package/src/agent/agent-experience.ts +3 -2
- package/src/agent/background-registry.ts +7 -3
- package/src/agent/cross-session/discovery.ts +5 -12
- package/src/agent/cross-session/file-inbox.ts +2 -2
- package/src/agent/effectiveness-tracker.ts +3 -2
- package/src/agent/sub-agent.ts +67 -6
- package/src/agent/types.ts +10 -0
- package/src/agent-view/agent-view-manager.ts +46 -0
- package/src/agent-view/dashboard.tsx +106 -16
- package/src/agent-view/session-view.tsx +128 -0
- package/src/commands/autoloop-journal.ts +6 -5
- package/src/commands/environment.ts +11 -8
- package/src/commands/project.ts +5 -3
- package/src/config/credential-crypto.ts +13 -1
- package/src/config/keys-manager.ts +7 -4
- package/src/config/loader.ts +144 -41
- package/src/config/preferences.ts +6 -3
- package/src/core/constitution-loader.ts +3 -2
- package/src/core/context.ts +35 -6
- package/src/core/crsi-producer.ts +4 -2
- package/src/core/crsi-sandbox.ts +2 -1
- package/src/core/dream-engine.ts +5 -11
- package/src/core/engine.ts +9 -4
- package/src/core/error-signature-db.ts +3 -2
- package/src/core/eval-harness.ts +3 -3
- package/src/core/hooks-executor.ts +60 -2
- package/src/core/instructions.ts +69 -48
- package/src/core/memory/memory-manager.ts +10 -6
- package/src/core/permission-audit.ts +13 -6
- package/src/core/permission-config.ts +28 -7
- package/src/core/permission-rules.ts +99 -5
- package/src/core/permission.ts +6 -1
- package/src/core/rule-engine.ts +3 -2
- package/src/core/session-log.ts +48 -11
- package/src/core/session-store.ts +64 -44
- package/src/daemon/attach-protocol.ts +30 -3
- package/src/daemon/auth.ts +4 -3
- package/src/daemon/index.ts +4 -3
- package/src/daemon/remote-engine.ts +173 -24
- package/src/daemon/server.ts +51 -1
- package/src/daemon/session-worker.ts +34 -1
- package/src/i18n-core/locales/en-US.json +1 -0
- package/src/i18n-core/locales/zh-CN.json +1 -0
- package/src/index.tsx +75 -17
- package/src/mcp/oauth.ts +47 -5
- package/src/mcp/token-store.ts +10 -11
- package/src/providers/openai-compat.ts +11 -0
- package/src/shared/arg-validation.ts +74 -2
- package/src/shared/package-info.ts +1 -1
- package/src/shared/regular-file.ts +63 -0
- package/src/shared/sanitize.ts +14 -2
- package/src/skills/bundled-skills.ts +1 -1
- package/src/tools/agent/memory.ts +4 -2
- package/src/tools/agent/workflow.ts +6 -3
- package/src/tools/exec/task.ts +82 -30
- package/src/tools/scheduling/cron.ts +3 -9
- package/src/ui/app.tsx +78 -14
- package/src/ui/commands.ts +73 -31
- package/src/ui/ctrl-c-confirm.ts +63 -0
- package/src/workflow/journal.ts +80 -26
package/src/core/session-log.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
4
|
import type { Message, ToolUseContent, ToolResultContent, ToolResult } from '../shared/types'
|
|
5
5
|
import type { CheckerDecision } from './post-flight-checker'
|
|
6
|
+
import { appendRegularFileSync, readRegularFileSync } from '../shared/regular-file'
|
|
6
7
|
import { miphamHome } from './paths.ts'
|
|
7
8
|
|
|
8
9
|
export type SessionEvent =
|
|
@@ -117,6 +118,9 @@ export function deriveMessages(events: SessionEvent[]): Message[] {
|
|
|
117
118
|
|
|
118
119
|
const LOG_DIR = miphamHome('sessions')
|
|
119
120
|
|
|
121
|
+
/** 一次性告警:日志路径不是普通文件(写不进去),每个进程只说一句。 */
|
|
122
|
+
let warnedNotAppendable = false
|
|
123
|
+
|
|
120
124
|
/** 将会话名消毒为安全文件名(与 SessionStore 共用;防路径穿越)。 */
|
|
121
125
|
export function sanitizeSessionName(name: string): string {
|
|
122
126
|
const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
@@ -147,22 +151,34 @@ export class SessionLog {
|
|
|
147
151
|
// Session logs contain full conversation + tool results (possibly credentials):
|
|
148
152
|
// restrict to owner-only (dir 0700, file 0600).
|
|
149
153
|
mkdirSync(LOG_DIR, { recursive: true, mode: 0o700 })
|
|
154
|
+
const path = join(LOG_DIR, `${sanitizeSessionName(this.name)}.jsonl`)
|
|
155
|
+
let wrote = 0
|
|
150
156
|
for (const e of this.buf.slice(this.flushed)) {
|
|
151
|
-
appendFileSync
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
// FIFO/socket 路径上「写不进去」而不是**干等**(`appendFileSync` 会打开目标写)。
|
|
158
|
+
// 写失败时**不推进** flushed:这些事件还算未落盘,路径恢复后下一次 save 会补上。
|
|
159
|
+
if (!appendRegularFileSync(path, JSON.stringify(e) + '\n', { mode: 0o600 })) {
|
|
160
|
+
if (!warnedNotAppendable) {
|
|
161
|
+
warnedNotAppendable = true
|
|
162
|
+
process.stderr.write(
|
|
163
|
+
`⚠ Mipham Code: ${path} is not a regular file — session log events were not written.\n`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
wrote++
|
|
156
169
|
}
|
|
157
|
-
this.flushed
|
|
170
|
+
this.flushed += wrote
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
/** 从既有 JSONL 打开,逐行解析为事件(已落盘事件标记为已 flush)。 */
|
|
161
174
|
static open(name: string): SessionLog {
|
|
162
175
|
const log = new SessionLog(name)
|
|
163
176
|
const path = join(LOG_DIR, `${sanitizeSessionName(name)}.jsonl`)
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
// 类型闸在读取之前:`SessionStore.load` 在快照读不出来时**回落到这里**,所以路径上
|
|
178
|
+
// 是个 FIFO 时不能在这里换个函数继续等 —— 读不动就当空日志(见 shared/regular-file.ts)。
|
|
179
|
+
const raw = readRegularFileSync(path)
|
|
180
|
+
if (raw === null) return log
|
|
181
|
+
for (const line of raw.split('\n')) {
|
|
166
182
|
const trimmed = line.trim()
|
|
167
183
|
if (!trimmed) continue
|
|
168
184
|
try {
|
|
@@ -178,6 +194,23 @@ export class SessionLog {
|
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
|
|
197
|
+
/**
|
|
198
|
+
* 消息形状闸 —— 投影与**出网**两侧都会解引用的字段的最低门槛,不是 schema 校验。
|
|
199
|
+
*
|
|
200
|
+
* 从前这里只查「`message` 是个对象」,于是 `{"type":"user/message","message":{}}`
|
|
201
|
+
* 一路活到线上:投影照原样收下它,provider 侧读到 `content` 为 `undefined` 走
|
|
202
|
+
* 「非字符串即 `ContentBlock[]`」那条分支,`.filter` 当场 TypeError(实测)。
|
|
203
|
+
* `content` 数组里的块同理 —— provider 会 `b.type` 逐个读。
|
|
204
|
+
*/
|
|
205
|
+
export function isValidMessage(m: unknown): boolean {
|
|
206
|
+
if (!m || typeof m !== 'object') return false
|
|
207
|
+
const msg = m as Record<string, unknown>
|
|
208
|
+
if (msg.role !== 'user' && msg.role !== 'assistant' && msg.role !== 'system') return false
|
|
209
|
+
const content = msg.content
|
|
210
|
+
if (typeof content === 'string') return true
|
|
211
|
+
return Array.isArray(content) && content.every((b) => !!b && typeof b === 'object')
|
|
212
|
+
}
|
|
213
|
+
|
|
181
214
|
/**
|
|
182
215
|
* 事件结构校验 —— 磁盘→内存的**唯一**入口(`open()`)用它挡掉坏行。
|
|
183
216
|
*
|
|
@@ -187,6 +220,9 @@ export class SessionLog {
|
|
|
187
220
|
* 却让投影抛 TypeError(rewrite 那条更狠:`out` 直接变 `undefined`,下一条就炸),
|
|
188
221
|
* 而 `/resume` 那条链上没有 try 兜它。坏行来自手写/拼接/半截重排,不是本进程写的。
|
|
189
222
|
*
|
|
223
|
+
* 带 message 的事件还要过 `isValidMessage`:投影只把它 `push` 进数组、自己不碰字段,
|
|
224
|
+
* 所以「投影不许崩」在这里**不够** —— 崩点在下游(provider 分支、UI 的 `msg.role`)。
|
|
225
|
+
*
|
|
190
226
|
* 不校验 `session/start`、`assistant/chunk`、`checker/decision`:投影对它们无分支;
|
|
191
227
|
* 未知类型一律放行(前向兼容 —— 认不出来不等于要销毁它)。
|
|
192
228
|
*/
|
|
@@ -197,7 +233,7 @@ function isValidEvent(e: unknown): e is SessionEvent {
|
|
|
197
233
|
switch (ev.type) {
|
|
198
234
|
case 'user/message':
|
|
199
235
|
case 'assistant/message':
|
|
200
|
-
return
|
|
236
|
+
return isValidMessage(ev.message)
|
|
201
237
|
case 'tool/call':
|
|
202
238
|
return (
|
|
203
239
|
typeof ev.id === 'string' &&
|
|
@@ -213,7 +249,8 @@ function isValidEvent(e: unknown): e is SessionEvent {
|
|
|
213
249
|
case 'compaction/summary':
|
|
214
250
|
return typeof ev.summary === 'string'
|
|
215
251
|
case 'compaction/rewrite':
|
|
216
|
-
|
|
252
|
+
// 快照替换:整份投影由它重建 ⇒ 元素形状与 message 事件同罪
|
|
253
|
+
return Array.isArray(ev.messages) && ev.messages.every(isValidMessage)
|
|
217
254
|
default:
|
|
218
255
|
return true
|
|
219
256
|
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
mkdirSync,
|
|
3
|
-
readdirSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
writeFileSync,
|
|
6
|
-
unlinkSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
existsSync,
|
|
9
|
-
statSync,
|
|
10
|
-
} from 'node:fs'
|
|
1
|
+
import { mkdirSync, readdirSync, unlinkSync, existsSync, statSync } from 'node:fs'
|
|
11
2
|
import { join } from 'node:path'
|
|
12
3
|
import { createHash } from 'node:crypto'
|
|
13
4
|
import type { Message } from '../shared/types'
|
|
14
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
sanitizeSessionName,
|
|
7
|
+
SessionLog,
|
|
8
|
+
deriveMessages,
|
|
9
|
+
messageToEvents,
|
|
10
|
+
isValidMessage,
|
|
11
|
+
} from './session-log'
|
|
12
|
+
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
13
|
+
import { readRegularFileSync } from '../shared/regular-file'
|
|
15
14
|
import { miphamHome } from './paths.ts'
|
|
16
15
|
|
|
17
16
|
export interface SessionMetadata {
|
|
@@ -54,6 +53,25 @@ function filePath(name: string): string {
|
|
|
54
53
|
return join(SESSIONS_DIR, `${sanitizeSessionName(name)}.jsonl`)
|
|
55
54
|
}
|
|
56
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 旧格式快照(单 JSON 对象 `{metadata, messages}`)的形状闸。
|
|
58
|
+
*
|
|
59
|
+
* `'metadata' in parsed` 拦不住 `{"metadata": null, …}` —— `in` 看的是**键在不在**,
|
|
60
|
+
* 而 `/resume <name>` 紧接着就读 `session.metadata.name`,那条链上没有 try(实测
|
|
61
|
+
* `TypeError`)。`messages` 的元素同理:`[null]` 会一路送到 UI 的
|
|
62
|
+
* `forwardedMessages.map((msg) => msg.role)`(`app.tsx:1041`)与 provider 的内容分支。
|
|
63
|
+
*
|
|
64
|
+
* 形状不对**整份返回 null**,而不是把坏元素过滤掉:调用方随即回落到事件日志那条路,
|
|
65
|
+
* 读不出来会如实报 load failed;过滤则会凭空造出一段残缺历史 —— 静默改写用户数据。
|
|
66
|
+
*/
|
|
67
|
+
function asStoredSnapshot(parsed: unknown): StoredSession | null {
|
|
68
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
69
|
+
const { metadata, messages } = parsed as { metadata?: unknown; messages?: unknown }
|
|
70
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) return null
|
|
71
|
+
if (!Array.isArray(messages) || !messages.every(isValidMessage)) return null
|
|
72
|
+
return { metadata: metadata as SessionMetadata, messages: messages as Message[] }
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
export class SessionStore {
|
|
58
76
|
/**
|
|
59
77
|
* Save a session as JSONL (one JSON object per line).
|
|
@@ -79,10 +97,11 @@ export class SessionStore {
|
|
|
79
97
|
messages,
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
// Atomic write: write to temp file, then rename (same-fs rename is atomic)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
// Atomic write: write to temp file, then rename (same-fs rename is atomic).
|
|
101
|
+
// 临时名由 atomicWriteFileSync 生成(pid + 随机后缀)—— 从前这里是固定的
|
|
102
|
+
// `path + '.tmp'`,两个进程同时存同一个会话时,先 rename 的那个会把**后一个**写者的
|
|
103
|
+
// 内容搬进目标,而它自己的 rename 再抛 ENOENT:在一个「目标永不半截」的函数里丢一次写。
|
|
104
|
+
atomicWriteFileSync(path, JSON.stringify(session) + '\n', { mode: 0o600 })
|
|
86
105
|
|
|
87
106
|
// Incremental index update — only touch this session's entry
|
|
88
107
|
try {
|
|
@@ -123,18 +142,12 @@ export class SessionStore {
|
|
|
123
142
|
/** 从磁盘重开一个 SessionLog(不存在返回空 log)。旧格式快照自动迁移为事件日志。 */
|
|
124
143
|
static loadLog(name: string): SessionLog {
|
|
125
144
|
const path = filePath(name)
|
|
126
|
-
if (!existsSync(path)) return SessionLog.open(name)
|
|
127
145
|
try {
|
|
128
|
-
const raw =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
typeof parsed === 'object' &&
|
|
133
|
-
'metadata' in parsed &&
|
|
134
|
-
Array.isArray((parsed as { messages?: unknown }).messages)
|
|
135
|
-
) {
|
|
146
|
+
const raw = readRegularFileSync(path)
|
|
147
|
+
if (raw === null) return SessionLog.open(name)
|
|
148
|
+
const old = asStoredSnapshot(JSON.parse(raw))
|
|
149
|
+
if (old) {
|
|
136
150
|
// 旧格式快照 → 迁移为事件日志(重写文件,避免后续 append 混格式)
|
|
137
|
-
const old = parsed as StoredSession
|
|
138
151
|
const log = new SessionLog(name)
|
|
139
152
|
log.append({
|
|
140
153
|
type: 'session/start',
|
|
@@ -160,19 +173,12 @@ export class SessionStore {
|
|
|
160
173
|
*/
|
|
161
174
|
static load(name: string): StoredSession | null {
|
|
162
175
|
const path = filePath(name)
|
|
163
|
-
if (!existsSync(path)) return null
|
|
164
|
-
|
|
165
176
|
try {
|
|
166
|
-
const raw =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
typeof parsed === 'object' &&
|
|
172
|
-
'metadata' in parsed &&
|
|
173
|
-
Array.isArray((parsed as { messages?: unknown }).messages)
|
|
174
|
-
) {
|
|
175
|
-
return parsed as StoredSession
|
|
177
|
+
const raw = readRegularFileSync(path)
|
|
178
|
+
if (raw !== null) {
|
|
179
|
+
// 旧格式:单 JSON 对象 {metadata, messages}
|
|
180
|
+
const old = asStoredSnapshot(JSON.parse(raw))
|
|
181
|
+
if (old) return old
|
|
176
182
|
}
|
|
177
183
|
} catch {
|
|
178
184
|
// 多行 JSONL → 新格式,走事件解析
|
|
@@ -295,7 +301,7 @@ export class SessionStore {
|
|
|
295
301
|
}
|
|
296
302
|
}
|
|
297
303
|
|
|
298
|
-
|
|
304
|
+
atomicWriteFileSync(INDEX_FILE, JSON.stringify(index, null, 2), { mode: 0o600 })
|
|
299
305
|
}
|
|
300
306
|
|
|
301
307
|
/**
|
|
@@ -323,7 +329,7 @@ export class SessionStore {
|
|
|
323
329
|
} else {
|
|
324
330
|
index.push(entry)
|
|
325
331
|
}
|
|
326
|
-
|
|
332
|
+
atomicWriteFileSync(INDEX_FILE, JSON.stringify(index, null, 2), { mode: 0o600 })
|
|
327
333
|
}
|
|
328
334
|
|
|
329
335
|
/**
|
|
@@ -340,8 +346,7 @@ export class SessionStore {
|
|
|
340
346
|
? `${safeRaw.slice(0, 80)}-${createHash('sha256').update(safeRaw).digest('hex').slice(0, 16)}`
|
|
341
347
|
: safeRaw
|
|
342
348
|
const summaryPath = join(SUMMARIES_DIR, `${safe}.md`)
|
|
343
|
-
|
|
344
|
-
encoding: 'utf-8',
|
|
349
|
+
atomicWriteFileSync(summaryPath, `# ${name}\n\n${summary}\n\nTags: ${tags.join(', ')}\n`, {
|
|
345
350
|
mode: 0o600,
|
|
346
351
|
})
|
|
347
352
|
|
|
@@ -365,7 +370,7 @@ export class SessionStore {
|
|
|
365
370
|
tags,
|
|
366
371
|
})
|
|
367
372
|
}
|
|
368
|
-
|
|
373
|
+
atomicWriteFileSync(INDEX_FILE, JSON.stringify(index, null, 2), { mode: 0o600 })
|
|
369
374
|
} catch {
|
|
370
375
|
// Index write is best-effort
|
|
371
376
|
}
|
|
@@ -414,11 +419,26 @@ export class SessionStore {
|
|
|
414
419
|
|
|
415
420
|
/**
|
|
416
421
|
* Read the raw index file, returning empty array if missing or corrupt.
|
|
422
|
+
*
|
|
423
|
+
* 「corrupt」不只是语法错:`null`、`{"a":1}`、`[1,2]` 都过得了 `JSON.parse`。放它们
|
|
424
|
+
* 过去,`getLatest()` 会按数组用它 —— `null` 在启动路径(`index.tsx` 建系统提示时)
|
|
425
|
+
* 抛 `TypeError: index.length`;`[1,2]` 更安静:`getLatest()` 返回数字 `1`,调用方接着
|
|
426
|
+
* 读 `latest.name` 拿到 `undefined`,再喂给 `load()` ⇒ `name.replace` 抛。条目还必须
|
|
427
|
+
* 有**字符串 name**:索引条目的第一用途就是当文件名用。
|
|
417
428
|
*/
|
|
418
429
|
private static loadIndexRaw(): SessionIndexEntry[] {
|
|
419
|
-
|
|
430
|
+
const raw = readRegularFileSync(INDEX_FILE)
|
|
431
|
+
if (raw === null) return []
|
|
420
432
|
try {
|
|
421
|
-
|
|
433
|
+
const parsed: unknown = JSON.parse(raw)
|
|
434
|
+
if (!Array.isArray(parsed)) return []
|
|
435
|
+
return parsed.filter(
|
|
436
|
+
(e): e is SessionIndexEntry =>
|
|
437
|
+
!!e &&
|
|
438
|
+
typeof e === 'object' &&
|
|
439
|
+
typeof (e as { name?: unknown }).name === 'string' &&
|
|
440
|
+
(e as { name: string }).name.length > 0,
|
|
441
|
+
)
|
|
422
442
|
} catch {
|
|
423
443
|
return []
|
|
424
444
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// apps/cli/src/daemon/attach-protocol.ts
|
|
2
|
-
// Client → Daemon: prompt, interrupt
|
|
3
|
-
// Daemon → Client: text, tool_use, tool_result, usage, task_notification, done, error,
|
|
2
|
+
// Client → Daemon: prompt, interrupt, set_mode
|
|
3
|
+
// Daemon → Client: text, tool_use, tool_result, usage, task_notification, done, error,
|
|
4
|
+
// session_state, mode
|
|
5
|
+
|
|
6
|
+
import type { PermissionMode } from '../shared/types'
|
|
4
7
|
|
|
5
8
|
export interface ClientPromptMessage {
|
|
6
9
|
type: 'prompt'
|
|
@@ -11,7 +14,18 @@ export interface ClientInterruptMessage {
|
|
|
11
14
|
type: 'interrupt'
|
|
12
15
|
sessionId: string
|
|
13
16
|
}
|
|
14
|
-
export
|
|
17
|
+
export interface ClientSetModeMessage {
|
|
18
|
+
type: 'set_mode'
|
|
19
|
+
sessionId: string
|
|
20
|
+
/**
|
|
21
|
+
* 请求的档位。daemon 会先过白名单、再走组织级限制的钳制,然后回播**生效**的那一档。
|
|
22
|
+
*
|
|
23
|
+
* `sessionId` 与 `prompt` / `interrupt` 一样是协议对称用的:daemon 一概以
|
|
24
|
+
* `ws.data.sessionId` 为准(否则一个 attach 就能改**别的**会话的闸门)。
|
|
25
|
+
*/
|
|
26
|
+
mode: PermissionMode
|
|
27
|
+
}
|
|
28
|
+
export type ClientMessage = ClientPromptMessage | ClientInterruptMessage | ClientSetModeMessage
|
|
15
29
|
|
|
16
30
|
export interface ServerTextMessage {
|
|
17
31
|
type: 'text'
|
|
@@ -61,6 +75,18 @@ export interface ServerSessionStateMessage {
|
|
|
61
75
|
provider: string
|
|
62
76
|
model: string
|
|
63
77
|
turnCount: number
|
|
78
|
+
/** 本会话当前**生效**的档位 —— 新 attach 的客户端据此初始化页脚,而不是猜 `default`。 */
|
|
79
|
+
mode: PermissionMode
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 回播生效档位。两个触发点:客户端发来 `set_mode`(含被拒的请求 —— 那时回播的是
|
|
83
|
+
* **当前**档),以及 `set_mode` 施加后。带的是 `PermissionSystem.getMode()` 的读数,
|
|
84
|
+
* 即**钳制之后**的值:报请求值就是那条老缺陷的形状 —— 说放行、实际审批。
|
|
85
|
+
*/
|
|
86
|
+
export interface ServerModeMessage {
|
|
87
|
+
type: 'mode'
|
|
88
|
+
sessionId: string
|
|
89
|
+
mode: PermissionMode
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
export type ServerMessage =
|
|
@@ -72,3 +98,4 @@ export type ServerMessage =
|
|
|
72
98
|
| ServerDoneMessage
|
|
73
99
|
| ServerErrorMessage
|
|
74
100
|
| ServerSessionStateMessage
|
|
101
|
+
| ServerModeMessage
|
package/src/daemon/auth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// apps/cli/src/daemon/auth.ts
|
|
2
2
|
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
3
|
-
import { readFileSync,
|
|
3
|
+
import { readFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
4
|
+
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
4
5
|
import { dirname } from 'node:path'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -21,7 +22,7 @@ export function loadOrCreateToken(tokenPath: string): string {
|
|
|
21
22
|
|
|
22
23
|
const token = generateToken()
|
|
23
24
|
mkdirSync(dirname(tokenPath), { recursive: true, mode: 0o700 })
|
|
24
|
-
|
|
25
|
+
atomicWriteFileSync(tokenPath, token, { mode: 0o600 })
|
|
25
26
|
return token
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -51,7 +52,7 @@ export function verifyToken(expected: string, provided: string): boolean {
|
|
|
51
52
|
*/
|
|
52
53
|
export function rotateToken(tokenPath: string): string {
|
|
53
54
|
const token = generateToken()
|
|
54
|
-
|
|
55
|
+
atomicWriteFileSync(tokenPath, token, { mode: 0o600 })
|
|
55
56
|
return token
|
|
56
57
|
}
|
|
57
58
|
|
package/src/daemon/index.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// getPort() reads the last known port from disk
|
|
7
7
|
|
|
8
8
|
import { join } from 'node:path'
|
|
9
|
-
import { existsSync, readFileSync,
|
|
9
|
+
import { existsSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs'
|
|
10
|
+
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
10
11
|
import { createServer as createNetServer } from 'node:net'
|
|
11
12
|
import type { Server } from 'bun'
|
|
12
13
|
import { DaemonDatabase } from './database'
|
|
@@ -247,8 +248,8 @@ export async function startDaemon(): Promise<{ port: number; token: string }> {
|
|
|
247
248
|
activeServer = server
|
|
248
249
|
|
|
249
250
|
// Write PID and port files
|
|
250
|
-
|
|
251
|
-
|
|
251
|
+
atomicWriteFileSync(PID_FILE, String(process.pid), { mode: 0o644 })
|
|
252
|
+
atomicWriteFileSync(PORT_FILE, String(port), { mode: 0o644 })
|
|
252
253
|
|
|
253
254
|
return { port, token }
|
|
254
255
|
}
|