@adhdev/mesh-shared 1.0.82-rc.4 → 1.0.82-rc.41
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/dist/assistant-protocol.d.ts +161 -6
- package/dist/decision-push-relay.d.ts +20 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1197 -118
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1107 -118
- package/dist/index.mjs.map +1 -1
- package/dist/launch-defaults.d.ts +196 -0
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/mesh-vocabulary.d.ts +13 -4
- package/dist/mission-git-state.d.ts +83 -0
- package/dist/mission-items.d.ts +202 -0
- package/dist/node-facts.d.ts +75 -1
- package/dist/outbound-message.d.ts +9 -2
- package/dist/path-ownership.d.ts +1 -1
- package/dist/protocol/daemon-server.d.ts +42 -2
- package/dist/protocol/dashboard-daemon-p2p.d.ts +1 -1
- package/dist/protocol/dashboard-server.d.ts +20 -2
- package/dist/provider-approval.d.ts +36 -0
- package/dist/provider-question.d.ts +19 -0
- package/dist/quota-freshness.d.ts +65 -0
- package/dist/relay-reprobe.d.ts +52 -0
- package/dist/release.d.ts +90 -0
- package/dist/role-launch.d.ts +11 -0
- package/dist/session-launch.d.ts +10 -22
- package/dist/slot-proposal.d.ts +14 -0
- package/dist/task-source-phase.d.ts +75 -0
- package/dist/turn-evidence.d.ts +29 -13
- package/dist/turn-ipc-mission.d.ts +68 -1
- package/dist/turn-ipc-queue.d.ts +19 -0
- package/dist/turn-ipc.d.ts +2 -2
- package/dist/types.d.ts +1 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/package.json +1 -1
- package/src/assistant-protocol.ts +302 -6
- package/src/daemon-normalize.ts +7 -5
- package/src/decision-push-relay.ts +23 -0
- package/src/index.ts +12 -0
- package/src/keyed-doc-delta.ts +9 -1
- package/src/launch-defaults.ts +170 -0
- package/src/mesh-tool-names.ts +16 -0
- package/src/mesh-vocabulary.ts +12 -2
- package/src/mission-git-state.ts +112 -0
- package/src/mission-items.ts +421 -0
- package/src/node-facts.ts +70 -1
- package/src/outbound-message.ts +19 -0
- package/src/path-ownership.ts +4 -6
- package/src/protocol/daemon-server.ts +37 -2
- package/src/protocol/dashboard-server.ts +13 -2
- package/src/provider-approval.ts +59 -0
- package/src/provider-question.ts +31 -0
- package/src/quota-freshness.ts +139 -0
- package/src/relay-reprobe.ts +117 -0
- package/src/release.ts +121 -0
- package/src/role-launch.ts +51 -0
- package/src/session-launch.ts +17 -1
- package/src/slot-proposal.ts +61 -2
- package/src/task-source-phase.ts +61 -0
- package/src/turn-evidence.ts +27 -20
- package/src/turn-ipc-mission.ts +121 -11
- package/src/turn-ipc-queue.ts +29 -1
- package/src/turn-ipc-turn.ts +1 -1
- package/src/turn-ipc.ts +1 -0
- package/src/types.ts +1 -0
- package/src/worker-protocol.ts +15 -0
|
@@ -11,22 +11,35 @@
|
|
|
11
11
|
* - the daemon-only verbs (launch / MCP-only pull / owner actions) that have
|
|
12
12
|
* no tool.
|
|
13
13
|
*
|
|
14
|
-
* Assistant tools
|
|
15
|
-
*
|
|
16
|
-
* never publish them.
|
|
14
|
+
* Assistant-specific tools remain separate from mesh tools. Driver discovery
|
|
15
|
+
* (list/describe) is shared across all three modes; invoke remains Worker-only.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
export const ASSISTANT_TOOLS = [
|
|
20
19
|
'projects',
|
|
20
|
+
'environment',
|
|
21
|
+
'assistant_action',
|
|
22
|
+
'project_watch',
|
|
21
23
|
'project_status',
|
|
22
24
|
'project_send',
|
|
23
25
|
'project_read',
|
|
24
26
|
'project_add',
|
|
27
|
+
'project_attach_remote',
|
|
25
28
|
'discover_repos',
|
|
26
29
|
'memory',
|
|
30
|
+
'memory_search',
|
|
27
31
|
'skill_view',
|
|
28
32
|
'skill_manage',
|
|
29
33
|
'project_note',
|
|
34
|
+
'launch_defaults',
|
|
35
|
+
'launch_defaults_set',
|
|
36
|
+
'driver_list',
|
|
37
|
+
'driver_describe',
|
|
38
|
+
'artifact_list',
|
|
39
|
+
'artifact_read',
|
|
40
|
+
'decision_answer',
|
|
41
|
+
'decision_request',
|
|
42
|
+
'session_control',
|
|
30
43
|
] as const
|
|
31
44
|
|
|
32
45
|
export type AssistantTool = typeof ASSISTANT_TOOLS[number]
|
|
@@ -40,16 +53,36 @@ export function isAssistantTool(value: unknown): value is AssistantTool {
|
|
|
40
53
|
/** Daemon verb names (§4.4 table + the five verbs below it). */
|
|
41
54
|
export const ASSISTANT_VERB = {
|
|
42
55
|
// tool verbs
|
|
56
|
+
driverList: 'driver_list',
|
|
57
|
+
driverDescribe: 'driver_describe',
|
|
43
58
|
projects: 'assistant_projects',
|
|
59
|
+
environment: 'assistant_environment',
|
|
60
|
+
action: 'assistant_action',
|
|
61
|
+
watch: 'assistant_project_watch',
|
|
62
|
+
executionState: 'assistant_execution_state',
|
|
63
|
+
executionSettings: 'assistant_execution_settings',
|
|
44
64
|
projectStatus: 'assistant_project_status',
|
|
45
65
|
projectSend: 'assistant_project_send',
|
|
46
66
|
projectRead: 'assistant_project_read',
|
|
47
67
|
projectAdd: 'assistant_project_add',
|
|
68
|
+
projectAttachRemote: 'assistant_project_attach_remote',
|
|
48
69
|
discoverRepos: 'assistant_discover_repos',
|
|
49
70
|
memory: 'assistant_memory',
|
|
71
|
+
/** Pull-only FTS search over the archive tier (memory design 2026-11-01 §3.4). */
|
|
72
|
+
memorySearch: 'assistant_memory_search',
|
|
50
73
|
skillView: 'assistant_skill_view',
|
|
51
74
|
skillManage: 'assistant_skill_manage',
|
|
52
75
|
projectNote: 'assistant_project_note',
|
|
76
|
+
launchDefaults: 'assistant_launch_defaults',
|
|
77
|
+
launchDefaultsSet: 'assistant_launch_defaults_set',
|
|
78
|
+
artifactList: 'assistant_artifact_list',
|
|
79
|
+
artifactRead: 'assistant_artifact_read',
|
|
80
|
+
/** Owner-decision inbox (structured-missions design 2026-10-28 §6.3): the owner's words recorded as answers. */
|
|
81
|
+
decisionAnswer: 'assistant_decision_answer',
|
|
82
|
+
/** Register a plain-text question a coordinator put to the owner as a decision item (§5.2). */
|
|
83
|
+
decisionRequest: 'assistant_decision_request',
|
|
84
|
+
/** Stop / restart / switch the provider of a coordinator, worker or the assistant session (handoff design 2026-11-01 §4). */
|
|
85
|
+
sessionControl: 'assistant_session_control',
|
|
53
86
|
// assistant-side daemon verbs without a tool of the same name
|
|
54
87
|
launch: 'launch_assistant',
|
|
55
88
|
pendingRelays: 'assistant_pending_relays',
|
|
@@ -57,22 +90,45 @@ export const ASSISTANT_VERB = {
|
|
|
57
90
|
stagedResolve: 'assistant_staged_resolve',
|
|
58
91
|
storeAdmin: 'assistant_store_admin',
|
|
59
92
|
importSkills: 'assistant_import_skills',
|
|
93
|
+
/** Owner skill list (attach/view counts, save proposals, recent skill metrics); reads move no counter. */
|
|
94
|
+
skillsList: 'assistant_skills_list',
|
|
95
|
+
/** Owner: what the assistant remembered recently, with undo handles (memory design §3.2). */
|
|
96
|
+
memoryDigest: 'assistant_memory_digest',
|
|
97
|
+
/** Owner: reverse one remembered write or one batch (memory design §3.2). Never an assistant tool. */
|
|
98
|
+
memoryUndo: 'assistant_memory_undo',
|
|
99
|
+
/** Projects the assistant is following (open relay threads) — list / stop following. */
|
|
100
|
+
threads: 'assistant_threads',
|
|
101
|
+
threadClose: 'assistant_thread_close',
|
|
60
102
|
} as const
|
|
61
103
|
|
|
62
104
|
export type AssistantVerb = typeof ASSISTANT_VERB[keyof typeof ASSISTANT_VERB]
|
|
63
105
|
|
|
64
106
|
/** The daemon verb each assistant tool calls. */
|
|
65
107
|
export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>> = {
|
|
108
|
+
driver_list: ASSISTANT_VERB.driverList,
|
|
109
|
+
driver_describe: ASSISTANT_VERB.driverDescribe,
|
|
66
110
|
projects: ASSISTANT_VERB.projects,
|
|
111
|
+
environment: ASSISTANT_VERB.environment,
|
|
112
|
+
assistant_action: ASSISTANT_VERB.action,
|
|
113
|
+
project_watch: ASSISTANT_VERB.watch,
|
|
67
114
|
project_status: ASSISTANT_VERB.projectStatus,
|
|
68
115
|
project_send: ASSISTANT_VERB.projectSend,
|
|
69
116
|
project_read: ASSISTANT_VERB.projectRead,
|
|
70
117
|
project_add: ASSISTANT_VERB.projectAdd,
|
|
118
|
+
project_attach_remote: ASSISTANT_VERB.projectAttachRemote,
|
|
71
119
|
discover_repos: ASSISTANT_VERB.discoverRepos,
|
|
72
120
|
memory: ASSISTANT_VERB.memory,
|
|
121
|
+
memory_search: ASSISTANT_VERB.memorySearch,
|
|
73
122
|
skill_view: ASSISTANT_VERB.skillView,
|
|
74
123
|
skill_manage: ASSISTANT_VERB.skillManage,
|
|
75
124
|
project_note: ASSISTANT_VERB.projectNote,
|
|
125
|
+
launch_defaults: ASSISTANT_VERB.launchDefaults,
|
|
126
|
+
launch_defaults_set: ASSISTANT_VERB.launchDefaultsSet,
|
|
127
|
+
artifact_list: ASSISTANT_VERB.artifactList,
|
|
128
|
+
artifact_read: ASSISTANT_VERB.artifactRead,
|
|
129
|
+
decision_answer: ASSISTANT_VERB.decisionAnswer,
|
|
130
|
+
decision_request: ASSISTANT_VERB.decisionRequest,
|
|
131
|
+
session_control: ASSISTANT_VERB.sessionControl,
|
|
76
132
|
}
|
|
77
133
|
|
|
78
134
|
/**
|
|
@@ -81,25 +137,32 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
|
|
|
81
137
|
* because they cause effects (a coordinator message or launch, a new
|
|
82
138
|
* project), and MCP clients use readOnlyHint to skip confirmation.
|
|
83
139
|
*/
|
|
84
|
-
export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['memory', 'skill_manage', 'project_note', 'project_send', 'project_add']
|
|
140
|
+
export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['project_watch', 'assistant_action', 'memory', 'skill_manage', 'project_note', 'project_send', 'project_add', 'project_attach_remote', 'launch_defaults_set', 'decision_answer', 'decision_request', 'session_control']
|
|
85
141
|
|
|
86
142
|
/** Owner-only verbs: never reachable from the assistant's MCP server (no `ipc`). */
|
|
87
143
|
export const ASSISTANT_OWNER_VERBS: readonly AssistantVerb[] = [
|
|
144
|
+
ASSISTANT_VERB.executionSettings,
|
|
88
145
|
ASSISTANT_VERB.stagedResolve,
|
|
89
146
|
ASSISTANT_VERB.storeAdmin,
|
|
90
147
|
ASSISTANT_VERB.importSkills,
|
|
148
|
+
ASSISTANT_VERB.skillsList,
|
|
149
|
+
ASSISTANT_VERB.memoryDigest,
|
|
150
|
+
ASSISTANT_VERB.memoryUndo,
|
|
151
|
+
ASSISTANT_VERB.threadClose,
|
|
91
152
|
]
|
|
92
153
|
|
|
93
154
|
/** The only assistant verbs accepted while an idle review turn is open (§4.10.7). */
|
|
94
155
|
export const ASSISTANT_REVIEW_TURN_VERBS: readonly AssistantVerb[] = [
|
|
95
156
|
ASSISTANT_VERB.memory,
|
|
157
|
+
ASSISTANT_VERB.memorySearch,
|
|
96
158
|
ASSISTANT_VERB.skillView,
|
|
97
159
|
ASSISTANT_VERB.skillManage,
|
|
98
160
|
ASSISTANT_VERB.projectNote,
|
|
99
161
|
]
|
|
100
162
|
|
|
101
|
-
|
|
102
|
-
|
|
163
|
+
import { canonicalDaemonId } from './daemon-normalize'
|
|
164
|
+
|
|
165
|
+
/** Env var the daemon sets on the assistant CLI's process (§4.5), read by the
|
|
103
166
|
* assistant MCP server and forwarded on every verb as
|
|
104
167
|
* `ASSISTANT_SESSION_ID_ARG`. Routing / ownership only — never an auth gate.
|
|
105
168
|
*/
|
|
@@ -107,3 +170,236 @@ export const ASSISTANT_SESSION_ID_ENV = 'ADHDEV_ASSISTANT_SESSION_ID'
|
|
|
107
170
|
|
|
108
171
|
/** Request arg carrying the calling assistant session id. */
|
|
109
172
|
export const ASSISTANT_SESSION_ID_ARG = 'assistantSessionId'
|
|
173
|
+
|
|
174
|
+
// ─── Remote Assistant Project Follow & Host-Authoritative Grant (rc24) ──────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Conceptual single-assistant slot in the verified account namespace.
|
|
178
|
+
* Fixed to `'personal'` — one logical assistant per account with one
|
|
179
|
+
* owner-selected active execution daemon binding per project ACL.
|
|
180
|
+
*/
|
|
181
|
+
export const ASSISTANT_ACCOUNT_SLOT = 'personal' as const
|
|
182
|
+
export type AssistantAccountSlot = typeof ASSISTANT_ACCOUNT_SLOT
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Daemon commands for remote project discovery, host ACL grant/revoke, and
|
|
186
|
+
* assistant-side follow/unfollow. Registered outside `ASSISTANT_VERB` so the
|
|
187
|
+
* assistant MCP server tool/verb parity table remains untouched.
|
|
188
|
+
*/
|
|
189
|
+
export const ASSISTANT_REMOTE_PROJECT_COMMAND = 'assistant_remote_project' as const
|
|
190
|
+
export const ASSISTANT_REMOTE_PROJECT_CATALOG_COMMAND = 'assistant_remote_project_catalog' as const
|
|
191
|
+
export const ASSISTANT_REMOTE_PROJECT_ACCESS_COMMAND = 'assistant_remote_project_access' as const
|
|
192
|
+
export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as const
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Narrow operations a non-roster follower deputy is permitted to invoke on
|
|
196
|
+
* `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
|
|
197
|
+
* `note`, `terminal`, and `inventory` remain strictly roster-member-only.
|
|
198
|
+
*/
|
|
199
|
+
export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send', 'artifact_list', 'artifact_read', 'decision_list', 'decision_answer', 'decision_request'] as const
|
|
200
|
+
export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
|
|
201
|
+
|
|
202
|
+
const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
|
|
203
|
+
|
|
204
|
+
export function isAssistantFollowerAllowedOp(value: unknown): value is AssistantFollowerAllowedOp {
|
|
205
|
+
return typeof value === 'string' && ASSISTANT_FOLLOWER_ALLOWED_OP_SET.has(value)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Owner-decision ops were added after grants were persisted with an explicit
|
|
210
|
+
* `allowedOps` list. A grant that already lets the follower read status (list)
|
|
211
|
+
* or send to the coordinator (answer / register) covers the matching decision op,
|
|
212
|
+
* so no existing grant needs re-issuing.
|
|
213
|
+
*/
|
|
214
|
+
const DECISION_OP_IMPLIED_BY: Readonly<Record<string, AssistantFollowerAllowedOp>> = {
|
|
215
|
+
decision_list: 'status',
|
|
216
|
+
decision_answer: 'send',
|
|
217
|
+
decision_request: 'send',
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function followerOpPermitted(op: string, allowedOps: readonly AssistantFollowerAllowedOp[]): boolean {
|
|
221
|
+
if (!isAssistantFollowerAllowedOp(op)) return false
|
|
222
|
+
if ((allowedOps as readonly string[]).includes(op)) return true
|
|
223
|
+
const implied = DECISION_OP_IMPLIED_BY[op]
|
|
224
|
+
return implied !== undefined && allowedOps.includes(implied)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Host-authoritative narrow project ACL persisted on the hosting daemon's
|
|
229
|
+
* `LocalMeshEntry` in `meshes.json`. Revocation increments `grantRevision`
|
|
230
|
+
* and retains a `'revoked'` tombstone so stale follower caches cannot replay.
|
|
231
|
+
*/
|
|
232
|
+
export interface AssistantRemoteAccessBinding {
|
|
233
|
+
hostDaemonId: string
|
|
234
|
+
meshId: string
|
|
235
|
+
accountSlot: AssistantAccountSlot
|
|
236
|
+
activeDaemonId: string
|
|
237
|
+
bindingRevision: number
|
|
238
|
+
grantRevision: number
|
|
239
|
+
status: 'active' | 'revoked'
|
|
240
|
+
allowedOps: readonly AssistantFollowerAllowedOp[]
|
|
241
|
+
allowCoordinatorLaunch?: boolean
|
|
242
|
+
allowArtifactRead?: boolean
|
|
243
|
+
grantedAt: number
|
|
244
|
+
revokedAt?: number
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Durable owner-binding receipt persisted in the assistant daemon's
|
|
249
|
+
* `assistant.json`. Survives ordinary daemon restarts and session/provider
|
|
250
|
+
* replacements (`bindSession`), while missing/corrupt/mismatched receipts
|
|
251
|
+
* fail closed until owner reconciliation.
|
|
252
|
+
*/
|
|
253
|
+
export interface AssistantOwnerBindingReceipt {
|
|
254
|
+
accountSlot: AssistantAccountSlot
|
|
255
|
+
daemonId: string
|
|
256
|
+
bindingRevision: number
|
|
257
|
+
boundAt: number | string
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type FollowedRemoteProjectState = 'followed' | 'unfollowed' | 'revoked'
|
|
261
|
+
|
|
262
|
+
export type RemoteAccessState =
|
|
263
|
+
| 'not_granted'
|
|
264
|
+
| 'granted_unfollowed'
|
|
265
|
+
| 'followed'
|
|
266
|
+
| 'revoked'
|
|
267
|
+
| 'host_unreachable'
|
|
268
|
+
| 'binding_mismatch'
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Assistant-local followed remote project reference persisted in `assistant.json`.
|
|
272
|
+
* Never injected into `meshes.json`, `mesh-host-records.json`, `mesh-node-state-push.json`,
|
|
273
|
+
* or `inlineMeshCache`.
|
|
274
|
+
*/
|
|
275
|
+
export interface FollowedRemoteProjectEntry {
|
|
276
|
+
/** Canonical key: `${canonicalDaemonId(hostDaemonId)}::${meshId}` */
|
|
277
|
+
key: string
|
|
278
|
+
hostDaemonId: string
|
|
279
|
+
hostLabel?: string
|
|
280
|
+
meshId: string
|
|
281
|
+
name: string
|
|
282
|
+
slug: string
|
|
283
|
+
sanitizedRepoIdentity: string | null
|
|
284
|
+
repoIdentity?: string
|
|
285
|
+
accountSlot: AssistantAccountSlot
|
|
286
|
+
bindingRevision: number
|
|
287
|
+
grantRevision: number
|
|
288
|
+
allowCoordinatorLaunch?: boolean
|
|
289
|
+
allowArtifactRead?: boolean
|
|
290
|
+
state: FollowedRemoteProjectState
|
|
291
|
+
followedAt: number | string
|
|
292
|
+
updatedAt: number | string
|
|
293
|
+
unfollowedAt?: number | string
|
|
294
|
+
revokedAt?: number | string
|
|
295
|
+
revokedReason?: string
|
|
296
|
+
lastCursor?: string | null
|
|
297
|
+
coordinatorSessionId?: string | null
|
|
298
|
+
lastErrorCode?: string
|
|
299
|
+
lastObserved?: {
|
|
300
|
+
observedAt: number | string
|
|
301
|
+
coordinatorState?: string
|
|
302
|
+
coordinatorMissing?: boolean
|
|
303
|
+
coordinatorPresent?: boolean
|
|
304
|
+
lastHeadline?: string
|
|
305
|
+
summary?: string
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Content-free canonical catalog metadata returned by `assistant_remote_project_catalog`
|
|
311
|
+
* for meshes actually hosted on the queried remote daemon.
|
|
312
|
+
*/
|
|
313
|
+
export interface RemoteProjectCatalogItem {
|
|
314
|
+
hostDaemonId: string
|
|
315
|
+
meshId: string
|
|
316
|
+
name: string
|
|
317
|
+
sanitizedRepoIdentity: string | null
|
|
318
|
+
coordinatorState?: 'none' | 'idle' | 'working' | 'waiting' | string
|
|
319
|
+
allowCoordinatorLaunch?: boolean
|
|
320
|
+
allowArtifactRead?: boolean
|
|
321
|
+
capabilities: {
|
|
322
|
+
status: true
|
|
323
|
+
read: true
|
|
324
|
+
poll: true
|
|
325
|
+
report: true
|
|
326
|
+
send: true
|
|
327
|
+
coordinatorPresent?: boolean
|
|
328
|
+
allowCoordinatorLaunch?: boolean
|
|
329
|
+
allowArtifactRead?: boolean
|
|
330
|
+
}
|
|
331
|
+
observedAt: number | string
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Build the canonical `(hostDaemonId, meshId)` key used across assistant follow state. */
|
|
335
|
+
export function followedRemoteProjectKey(hostDaemonId: string, meshId: string): string {
|
|
336
|
+
const canonHost = canonicalDaemonId(hostDaemonId) ?? hostDaemonId.trim()
|
|
337
|
+
return `${canonHost}::${meshId.trim()}`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sanitize a repository identity or remote URL into a content-free `host/path`
|
|
342
|
+
* identifier suitable for remote discovery catalogs. Strips credentials, query
|
|
343
|
+
* strings, fragments, and `.git` suffixes, and rejects local filesystem paths.
|
|
344
|
+
*/
|
|
345
|
+
export function sanitizeRemoteRepoIdentity(raw: string | null | undefined): string | null {
|
|
346
|
+
if (typeof raw !== 'string') return null
|
|
347
|
+
const trimmed = raw.trim()
|
|
348
|
+
if (!trimmed) return null
|
|
349
|
+
|
|
350
|
+
// Reject local filesystem paths (Unix absolute/relative/home, Windows drive/UNC, file:// URLs).
|
|
351
|
+
if (
|
|
352
|
+
trimmed.startsWith('/')
|
|
353
|
+
|| trimmed.startsWith('./')
|
|
354
|
+
|| trimmed.startsWith('../')
|
|
355
|
+
|| trimmed.startsWith('~/')
|
|
356
|
+
|| trimmed.startsWith('\\')
|
|
357
|
+
|| /^[a-zA-Z]:[\\/]/.test(trimmed)
|
|
358
|
+
|| /^file:/i.test(trimmed)
|
|
359
|
+
) {
|
|
360
|
+
return null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const withoutQuery = trimmed.replace(/[?#].*$/, '').replace(/\/+$/, '')
|
|
364
|
+
if (!withoutQuery) return null
|
|
365
|
+
|
|
366
|
+
let host = ''
|
|
367
|
+
let repoPath = ''
|
|
368
|
+
|
|
369
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(withoutQuery)) {
|
|
370
|
+
try {
|
|
371
|
+
const url = new URL(withoutQuery)
|
|
372
|
+
if (url.protocol === 'file:') return null
|
|
373
|
+
host = url.hostname.toLowerCase()
|
|
374
|
+
repoPath = decodeURIComponent(url.pathname)
|
|
375
|
+
.replace(/^\/+|\/+$/g, '')
|
|
376
|
+
.replace(/\.git$/i, '')
|
|
377
|
+
} catch {
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
const scpMatch = withoutQuery.match(/^(?:[^@/:]+@)?(\[[^\]]+\]|[^/:]+):(.+)$/)
|
|
382
|
+
if (scpMatch) {
|
|
383
|
+
host = scpMatch[1].replace(/^\[|\]$/g, '').toLowerCase()
|
|
384
|
+
repoPath = scpMatch[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
|
|
385
|
+
} else {
|
|
386
|
+
const slash = withoutQuery.indexOf('/')
|
|
387
|
+
if (slash > 0) {
|
|
388
|
+
const rawHost = withoutQuery.slice(0, slash)
|
|
389
|
+
const atIdx = rawHost.lastIndexOf('@')
|
|
390
|
+
host = (atIdx >= 0 ? rawHost.slice(atIdx + 1) : rawHost).toLowerCase()
|
|
391
|
+
repoPath = withoutQuery.slice(slash + 1).replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!host || !repoPath) return null
|
|
397
|
+
if (!host.includes('.') && host !== 'localhost') return null
|
|
398
|
+
if (/[\s@:\\]/.test(host)) return null
|
|
399
|
+
const segments = repoPath.split('/').filter(Boolean)
|
|
400
|
+
if (segments.length === 0) return null
|
|
401
|
+
if (segments.some(seg => seg === '.' || seg === '..' || /[\s@:\\]/.test(seg))) return null
|
|
402
|
+
|
|
403
|
+
return `${host}/${segments.join('/')}`.slice(0, 200)
|
|
404
|
+
}
|
|
405
|
+
|
package/src/daemon-normalize.ts
CHANGED
|
@@ -108,13 +108,15 @@ export function isPhantomDaemonEntry(
|
|
|
108
108
|
export function machineCoreFromDaemonId(id: string | null | undefined): string | undefined {
|
|
109
109
|
const trimmed = readString(id)
|
|
110
110
|
if (!trimmed) return undefined
|
|
111
|
+
const lower = trimmed.toLowerCase()
|
|
111
112
|
for (const prefix of DAEMON_ID_PREFIXES) {
|
|
112
|
-
if (
|
|
113
|
+
if (lower.startsWith(prefix)) {
|
|
113
114
|
const core = trimmed.slice(prefix.length).trim()
|
|
114
|
-
|
|
115
|
+
if (!core) return undefined
|
|
116
|
+
return /^mach_/i.test(core) ? `mach_${core.slice(5)}` : core
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
|
-
return trimmed
|
|
119
|
+
return /^mach_/i.test(trimmed) ? `mach_${trimmed.slice(5)}` : trimmed
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
@@ -144,8 +146,8 @@ export function machineCoreFromDaemonId(id: string | null | undefined): string |
|
|
|
144
146
|
export function canonicalDaemonId(id: string | null | undefined): string | undefined {
|
|
145
147
|
const core = machineCoreFromDaemonId(id)
|
|
146
148
|
if (!core) return undefined
|
|
147
|
-
if (
|
|
148
|
-
return `
|
|
149
|
+
if (!/^mach_/i.test(core)) return core
|
|
150
|
+
return `daemon_mach_${core.slice(5)}`
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
/** True when both ids resolve to the same machine core — i.e. they are the same
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in "answer a decision from the push notification" relay (mission 4ae3edf2, owner 2026-10-29).
|
|
3
|
+
*
|
|
4
|
+
* ★ A deliberate, narrow exception to "no dashboard command relay over server WS" (CLAUDE.md).
|
|
5
|
+
* With the user's opt-in setting ON and no dashboard window open, the notification's
|
|
6
|
+
* recommended-option action is POSTed by the service worker to the server, which forwards it to the
|
|
7
|
+
* owning daemon over the existing daemon WS as ONE fixed command. What crosses the server is exactly
|
|
8
|
+
* `{ meshId, itemId, optionId }` (+ the single-use answer token that authorizes it) — never the
|
|
9
|
+
* question, context or free text. Shared so the server's frame and the daemon's ingress cannot drift.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Daemon command the server sends; its only accepted source is {@link DECISION_PUSH_RELAY_SOURCE}. */
|
|
13
|
+
export const DECISION_PUSH_RELAY_COMMAND = 'mission_decision_push_relay_answer'
|
|
14
|
+
|
|
15
|
+
/** Command source the server stamps on the relayed frame (never settable by REST/API callers). */
|
|
16
|
+
export const DECISION_PUSH_RELAY_SOURCE = 'push_relay'
|
|
17
|
+
|
|
18
|
+
/** The complete relayed payload — ids only. */
|
|
19
|
+
export type DecisionPushRelayPayload = {
|
|
20
|
+
meshId: string
|
|
21
|
+
itemId: string
|
|
22
|
+
optionId: string
|
|
23
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './git-normalize'
|
|
|
16
16
|
export * from './session-normalize'
|
|
17
17
|
export * from './node-normalize'
|
|
18
18
|
export * from './node-facts'
|
|
19
|
+
export * from './quota-freshness'
|
|
19
20
|
export * from './workspace-normalize'
|
|
20
21
|
export * from './daemon-normalize'
|
|
21
22
|
export * from './git-summarize'
|
|
@@ -40,4 +41,15 @@ export * from './session-launch'
|
|
|
40
41
|
export * from './turn-ipc'
|
|
41
42
|
export * from './path-ownership'
|
|
42
43
|
export * from './mission-brief'
|
|
44
|
+
export * from './mission-items'
|
|
45
|
+
export * from './mission-git-state'
|
|
46
|
+
export * from './release'
|
|
47
|
+
export * from './decision-push-relay'
|
|
43
48
|
export * from './keyed-doc-delta'
|
|
49
|
+
export * from './provider-approval'
|
|
50
|
+
export * from './provider-question'
|
|
51
|
+
export * from './launch-defaults'
|
|
52
|
+
|
|
53
|
+
export * from './role-launch';
|
|
54
|
+
export * from './task-source-phase';
|
|
55
|
+
export * from './relay-reprobe'
|
package/src/keyed-doc-delta.ts
CHANGED
|
@@ -326,7 +326,15 @@ export function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta:
|
|
|
326
326
|
const heldList = heldCollections.get(path)
|
|
327
327
|
if (change?.absent) {
|
|
328
328
|
const [head, tail] = splitPath(path)
|
|
329
|
-
if (!tail)
|
|
329
|
+
if (!tail) {
|
|
330
|
+
delete next[head]
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
const parent = next[head]
|
|
334
|
+
if (!isRecord(parent) || !(tail in parent)) continue
|
|
335
|
+
const copy = { ...parent }
|
|
336
|
+
delete copy[tail]
|
|
337
|
+
next[head] = copy
|
|
330
338
|
continue
|
|
331
339
|
}
|
|
332
340
|
if (!change && heldList === undefined) continue
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Daemon-local launch intent and command wire contract. P2P/local only;
|
|
2
|
+
* never include this document or its history in cloud status_report.
|
|
3
|
+
* Publication through mesh-shared's barrel is a later integration step. */
|
|
4
|
+
export const LAUNCH_DEFAULT_ROLES = ['assistant', 'coordinator'] as const;
|
|
5
|
+
export type LaunchDefaultRole = typeof LAUNCH_DEFAULT_ROLES[number];
|
|
6
|
+
export const LAUNCH_DEFAULT_SOURCES = ['user', 'stored_default', 'project_default', 'provider_default'] as const;
|
|
7
|
+
export type LaunchDefaultSource = typeof LAUNCH_DEFAULT_SOURCES[number];
|
|
8
|
+
export const LAUNCH_DEFAULTS_NEXT_LAUNCH_NOTE = 'Applies to the next launch; running sessions are unchanged.';
|
|
9
|
+
export function isLaunchDefaultRole(value: unknown): value is LaunchDefaultRole {
|
|
10
|
+
return typeof value === 'string' && (LAUNCH_DEFAULT_ROLES as readonly string[]).includes(value);
|
|
11
|
+
}
|
|
12
|
+
export function isLaunchDefaultSource(value: unknown): value is LaunchDefaultSource {
|
|
13
|
+
return typeof value === 'string' && (LAUNCH_DEFAULT_SOURCES as readonly string[]).includes(value);
|
|
14
|
+
}
|
|
15
|
+
export interface LaunchDefaultSelection {
|
|
16
|
+
provider: string;
|
|
17
|
+
/** Omission means pass no model/thinking argument, not inherit another provider. */
|
|
18
|
+
model?: string;
|
|
19
|
+
thinkingLevel?: string;
|
|
20
|
+
}
|
|
21
|
+
export type LaunchDefaultToolRestriction = 'enforced' | 'prompt_only';
|
|
22
|
+
export type LaunchDefaultOrigin = 'owner' | 'human' | 'relay' | 'review' | 'review_tainted';
|
|
23
|
+
export interface LaunchDefaultEntry extends LaunchDefaultSelection {
|
|
24
|
+
revision: number;
|
|
25
|
+
setBy: 'owner' | 'assistant';
|
|
26
|
+
setAt: number;
|
|
27
|
+
/** Ordered quota-failover alternatives (<= 3, distinct providers). Absent = none. */
|
|
28
|
+
fallback?: LaunchDefaultSelection[];
|
|
29
|
+
}
|
|
30
|
+
export interface LaunchDefaultChange {
|
|
31
|
+
at: number;
|
|
32
|
+
role: LaunchDefaultRole;
|
|
33
|
+
by: 'owner' | 'assistant';
|
|
34
|
+
origin: LaunchDefaultOrigin;
|
|
35
|
+
from: LaunchDefaultEntry | null;
|
|
36
|
+
to: LaunchDefaultEntry | null;
|
|
37
|
+
}
|
|
38
|
+
export interface LaunchDefaultsDocument {
|
|
39
|
+
version: 1;
|
|
40
|
+
/** Global document token, including clears; distinct from entry revisions. */
|
|
41
|
+
revision: number;
|
|
42
|
+
assistant?: LaunchDefaultEntry;
|
|
43
|
+
coordinator?: LaunchDefaultEntry;
|
|
44
|
+
history: LaunchDefaultChange[];
|
|
45
|
+
}
|
|
46
|
+
export interface LaunchDefaultRef {
|
|
47
|
+
role: LaunchDefaultRole;
|
|
48
|
+
scope: 'global' | 'project';
|
|
49
|
+
/** Entry revision for global defaults; host project token for project defaults. */
|
|
50
|
+
revision: number;
|
|
51
|
+
projectId?: string;
|
|
52
|
+
}
|
|
53
|
+
export type LaunchDefaultField = keyof LaunchDefaultSelection;
|
|
54
|
+
export interface LaunchDefaultAxis {
|
|
55
|
+
value: string | null;
|
|
56
|
+
source: LaunchDefaultSource;
|
|
57
|
+
defaultRef?: LaunchDefaultRef;
|
|
58
|
+
}
|
|
59
|
+
export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
|
|
60
|
+
export type LaunchDefaultValidationReason =
|
|
61
|
+
| 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled'
|
|
62
|
+
| 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported'
|
|
63
|
+
| 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported'
|
|
64
|
+
| 'thinking_not_offered' | 'catalog_unavailable';
|
|
65
|
+
export type LaunchDefaultResolution =
|
|
66
|
+
| { ok: true; selection: LaunchDefaultSelection; applied: Record<LaunchDefaultField, LaunchDefaultAxis>;
|
|
67
|
+
defaultRef?: LaunchDefaultRef; documentRevision: number; warnings: LaunchDefaultWarning[];
|
|
68
|
+
toolRestriction: LaunchDefaultToolRestriction }
|
|
69
|
+
| { ok: false; code: 'launch_default_required' | 'launch_default_unavailable' | 'launch_selection_invalid';
|
|
70
|
+
role: LaunchDefaultRole; field: LaunchDefaultField; reason: string; source?: LaunchDefaultSource;
|
|
71
|
+
value?: string | null; defaultRef?: LaunchDefaultRef; candidates?: readonly string[]; hint: string };
|
|
72
|
+
export interface LaunchDefaultsRoleView {
|
|
73
|
+
stored: LaunchDefaultEntry | null;
|
|
74
|
+
/** The existing daemon resolver's result, including loud failures. */
|
|
75
|
+
effective: LaunchDefaultResolution;
|
|
76
|
+
}
|
|
77
|
+
type Target = { role: 'assistant'; project?: never } | { role: 'coordinator'; project?: string };
|
|
78
|
+
export type LaunchDefaultsReadRequest =
|
|
79
|
+
| ({ action?: 'get' } & (Target | { role?: never; project?: never }))
|
|
80
|
+
| ({ action: 'check' } & Target & LaunchDefaultSelection);
|
|
81
|
+
/** Mandatory CAS: global document token, or host project token for project writes.
|
|
82
|
+
* 0 is explicit first setup; clears advance the token and never restore fallback. */
|
|
83
|
+
export type LaunchDefaultsSetRequest = Target & { expectedRevision: number } & (
|
|
84
|
+
| ({ action: 'set' } & LaunchDefaultSelection)
|
|
85
|
+
| { action: 'clear'; provider?: never; model?: never; thinkingLevel?: never }
|
|
86
|
+
);
|
|
87
|
+
/** Existing assistant routing envelope; session identity never supplies origin. */
|
|
88
|
+
export type AssistantLaunchDefaultsReadRequest = LaunchDefaultsReadRequest & { assistantSessionId?: string };
|
|
89
|
+
export type AssistantLaunchDefaultsSetRequest = LaunchDefaultsSetRequest & { assistantSessionId?: string };
|
|
90
|
+
export const LAUNCH_DEFAULT_ERROR_CODES = [
|
|
91
|
+
'invalid_args', 'launch_default_required', 'launch_default_unavailable', 'launch_selection_invalid',
|
|
92
|
+
'launch_default_invalid', 'revision_conflict', 'store_unavailable', 'store_busy', 'persistence_failed',
|
|
93
|
+
'project_not_found', 'project_ambiguous', 'project_not_hosted', 'project_storage_unavailable',
|
|
94
|
+
'staging_unavailable', 'staging_failed', 'review_turn_tool_denied',
|
|
95
|
+
] as const;
|
|
96
|
+
export type LaunchDefaultsErrorCode = typeof LAUNCH_DEFAULT_ERROR_CODES[number];
|
|
97
|
+
export interface LaunchDefaultsError {
|
|
98
|
+
success: false;
|
|
99
|
+
code: LaunchDefaultsErrorCode;
|
|
100
|
+
error: string;
|
|
101
|
+
reason?: string;
|
|
102
|
+
role?: LaunchDefaultRole;
|
|
103
|
+
field?: LaunchDefaultField;
|
|
104
|
+
source?: LaunchDefaultSource;
|
|
105
|
+
value?: string | null;
|
|
106
|
+
defaultRef?: LaunchDefaultRef;
|
|
107
|
+
candidates?: readonly string[];
|
|
108
|
+
actualRevision?: number;
|
|
109
|
+
/** 0 for an absent global document; absent when it cannot be read. */
|
|
110
|
+
documentRevision?: number;
|
|
111
|
+
/** A persistence failure can replace intent without confirming durability.
|
|
112
|
+
* Re-read before retrying; never report applied on committed:true alone. */
|
|
113
|
+
committed?: boolean;
|
|
114
|
+
}
|
|
115
|
+
export interface LaunchDefaultsReadSuccess {
|
|
116
|
+
success: true;
|
|
117
|
+
result: 'get';
|
|
118
|
+
documentRevision: number;
|
|
119
|
+
roles: Partial<Record<LaunchDefaultRole, LaunchDefaultsRoleView>>;
|
|
120
|
+
project?: { projectId: string; revision: number; entry: LaunchDefaultEntry | null };
|
|
121
|
+
}
|
|
122
|
+
export interface LaunchDefaultsCheckSuccess {
|
|
123
|
+
success: true;
|
|
124
|
+
result: 'check';
|
|
125
|
+
role: LaunchDefaultRole;
|
|
126
|
+
selection: LaunchDefaultSelection;
|
|
127
|
+
warnings: LaunchDefaultWarning[];
|
|
128
|
+
toolRestriction: LaunchDefaultToolRestriction;
|
|
129
|
+
lowersToolLock: boolean;
|
|
130
|
+
}
|
|
131
|
+
export interface LaunchDefaultsApplied {
|
|
132
|
+
success: true;
|
|
133
|
+
result: 'applied';
|
|
134
|
+
role: LaunchDefaultRole;
|
|
135
|
+
scope: 'global' | 'project';
|
|
136
|
+
projectId?: string;
|
|
137
|
+
entry: LaunchDefaultEntry | null;
|
|
138
|
+
previous: LaunchDefaultEntry | null;
|
|
139
|
+
/** CAS token returned by the authority that performed the write. */
|
|
140
|
+
revision: number;
|
|
141
|
+
warnings: LaunchDefaultWarning[];
|
|
142
|
+
durable: true;
|
|
143
|
+
note: string;
|
|
144
|
+
}
|
|
145
|
+
export interface LaunchDefaultsStaged {
|
|
146
|
+
success: true;
|
|
147
|
+
result: 'staged';
|
|
148
|
+
role: LaunchDefaultRole;
|
|
149
|
+
stagedId: string;
|
|
150
|
+
previous: LaunchDefaultEntry | null;
|
|
151
|
+
selection: LaunchDefaultSelection | null;
|
|
152
|
+
expectedRevision: number;
|
|
153
|
+
warnings: LaunchDefaultWarning[];
|
|
154
|
+
note: string;
|
|
155
|
+
}
|
|
156
|
+
/** Existing owner Inbox adapter stores this proposal; it is not another default.
|
|
157
|
+
* Applying it must revalidate on the host and CAS via the same set handler. */
|
|
158
|
+
export interface LaunchDefaultsProposal {
|
|
159
|
+
kind: 'launch_defaults';
|
|
160
|
+
role: LaunchDefaultRole;
|
|
161
|
+
project?: string;
|
|
162
|
+
selection: LaunchDefaultSelection | null;
|
|
163
|
+
previous: LaunchDefaultEntry | null;
|
|
164
|
+
expectedRevision: number;
|
|
165
|
+
origin: LaunchDefaultOrigin;
|
|
166
|
+
callerSessionId?: string;
|
|
167
|
+
}
|
|
168
|
+
export type LaunchDefaultsReadResponse = LaunchDefaultsReadSuccess | LaunchDefaultsCheckSuccess | LaunchDefaultsError;
|
|
169
|
+
export type LaunchDefaultsSetResponse = LaunchDefaultsApplied | LaunchDefaultsStaged | LaunchDefaultsError;
|
|
170
|
+
export type LaunchDefaultsResponse = LaunchDefaultsReadResponse | LaunchDefaultsSetResponse;
|