@adhdev/mesh-shared 1.0.82-rc.22 → 1.0.82-rc.24
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 +124 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +286 -108
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +275 -108
- package/dist/index.mjs.map +1 -1
- package/dist/launch-defaults.d.ts +1 -1
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/outbound-message.d.ts +7 -0
- package/dist/protocol/daemon-server.d.ts +6 -0
- package/dist/protocol/dashboard-daemon-p2p.d.ts +1 -1
- package/dist/protocol/dashboard-server.d.ts +10 -0
- package/dist/role-launch.d.ts +11 -0
- package/dist/session-launch.d.ts +3 -22
- package/dist/slot-proposal.d.ts +14 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/package.json +1 -1
- package/src/assistant-protocol.ts +216 -5
- package/src/index.ts +2 -0
- package/src/launch-defaults.ts +1 -1
- package/src/mesh-tool-names.ts +3 -0
- package/src/outbound-message.ts +17 -0
- package/src/protocol/daemon-server.ts +4 -2
- package/src/protocol/dashboard-server.ts +8 -2
- package/src/role-launch.ts +51 -0
- package/src/session-launch.ts +8 -1
- package/src/slot-proposal.ts +61 -2
- package/src/worker-protocol.ts +4 -0
|
@@ -51,7 +51,7 @@ export interface LaunchDefaultAxis {
|
|
|
51
51
|
defaultRef?: LaunchDefaultRef;
|
|
52
52
|
}
|
|
53
53
|
export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
|
|
54
|
-
export type LaunchDefaultValidationReason = 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled' | 'provider_not_detected' | 'provider_not_eligible' | 'model_unsupported' | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported' | 'thinking_not_offered' | 'catalog_unavailable';
|
|
54
|
+
export type LaunchDefaultValidationReason = 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled' | 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported' | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported' | 'thinking_not_offered' | 'catalog_unavailable';
|
|
55
55
|
export type LaunchDefaultResolution = {
|
|
56
56
|
ok: true;
|
|
57
57
|
selection: LaunchDefaultSelection;
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* Order mirrors mcp-server `ALL_MESH_TOOLS` for easy visual diffing, but the consistency
|
|
19
19
|
* checks are set-based (order-insensitive).
|
|
20
20
|
*/
|
|
21
|
-
export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots"];
|
|
21
|
+
export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list"];
|
|
22
22
|
export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
|
|
23
23
|
/** The count the `NN tools` barrel doc comments and consistency test assert against. */
|
|
24
|
-
export declare const CANONICAL_MESH_TOOL_COUNT:
|
|
24
|
+
export declare const CANONICAL_MESH_TOOL_COUNT: 44;
|
|
25
25
|
/**
|
|
26
26
|
* Tool names retired by the 2026-09-26 tool-surface consolidation (60 → 48).
|
|
27
27
|
* Every capability behind them is still reachable: each retired name maps to the
|
|
@@ -203,3 +203,10 @@ export declare function isSubmitOutcome(value: unknown): value is SubmitOutcome;
|
|
|
203
203
|
export declare function isInputEnvelopeWire(value: unknown): value is InputEnvelopeWire;
|
|
204
204
|
/** Minimal structural check for the wire boundary — not a full decoder (mesh-shared stays a pure leaf; semantic validation stays with the handler that owns it). */
|
|
205
205
|
export declare function isOutboundMessage(value: unknown): value is OutboundMessage;
|
|
206
|
+
/**
|
|
207
|
+
* Drop the `<pasted_content>` envelope lines Claude Code wraps around a pasted
|
|
208
|
+
* input. Only a text that OPENS with the envelope is touched; anything else is
|
|
209
|
+
* returned unchanged. Shared across daemon transcript merge and web echo/display
|
|
210
|
+
* normalization so wrapped turns correlate identically.
|
|
211
|
+
*/
|
|
212
|
+
export declare function unwrapPastedContent(text: string): string;
|
|
@@ -213,11 +213,15 @@ export type P2POfferWirePayload = {
|
|
|
213
213
|
sdp: string;
|
|
214
214
|
type: string;
|
|
215
215
|
peerId: string;
|
|
216
|
+
attemptId?: number;
|
|
217
|
+
attemptEpoch?: string;
|
|
216
218
|
};
|
|
217
219
|
export type P2PIceWirePayload = {
|
|
218
220
|
candidate: string;
|
|
219
221
|
mid: string;
|
|
220
222
|
peerId: string;
|
|
223
|
+
attemptId?: number;
|
|
224
|
+
attemptEpoch?: string;
|
|
221
225
|
};
|
|
222
226
|
/**
|
|
223
227
|
* A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
|
|
@@ -229,6 +233,8 @@ export type P2PIceWirePayload = {
|
|
|
229
233
|
export type RelayedP2PSignalWirePayload = {
|
|
230
234
|
peerId?: string;
|
|
231
235
|
daemonId?: string;
|
|
236
|
+
attemptId?: number;
|
|
237
|
+
attemptEpoch?: string;
|
|
232
238
|
sharePermission?: string;
|
|
233
239
|
shareTargetSessionId?: string;
|
|
234
240
|
sdp?: string;
|
|
@@ -34,7 +34,7 @@ export type ChunkFrame<T extends string> = {
|
|
|
34
34
|
export declare function isChunkType(value: unknown): value is `${string}_chunk`;
|
|
35
35
|
export declare const DASHBOARD_TO_DAEMON_P2P_TYPES: readonly ["ping", "pong", "command", "input", "pty_input", "pty_resize", "screenshot_start", "screenshot_stop", "subscribe", "unsubscribe", "seqscribe_session_interest", "read", "write", "list"];
|
|
36
36
|
export type DashboardToDaemonP2PType = typeof DASHBOARD_TO_DAEMON_P2P_TYPES[number];
|
|
37
|
-
export declare const isDashboardToDaemonP2PType: (value: unknown) => value is "list" | "command" | "ping" | "pong" | "input" | "pty_input" | "pty_resize" | "screenshot_start" | "screenshot_stop" | "subscribe" | "unsubscribe" | "seqscribe_session_interest" | "
|
|
37
|
+
export declare const isDashboardToDaemonP2PType: (value: unknown) => value is "read" | "list" | "command" | "ping" | "pong" | "input" | "pty_input" | "pty_resize" | "screenshot_start" | "screenshot_stop" | "subscribe" | "unsubscribe" | "seqscribe_session_interest" | "write";
|
|
38
38
|
export type DashboardToDaemonChunkType = `${DashboardToDaemonP2PType}_chunk`;
|
|
39
39
|
export declare function isDashboardToDaemonChunkType(value: unknown): value is DashboardToDaemonChunkType;
|
|
40
40
|
export type P2PPingMsg = {
|
|
@@ -109,12 +109,16 @@ export type SystemAnnouncementWirePayload = {
|
|
|
109
109
|
export type DashboardP2POfferWirePayload = {
|
|
110
110
|
daemonId: string;
|
|
111
111
|
peerId?: string;
|
|
112
|
+
attemptId?: number;
|
|
113
|
+
attemptEpoch?: string;
|
|
112
114
|
sdp: string;
|
|
113
115
|
type: string;
|
|
114
116
|
};
|
|
115
117
|
export type DashboardP2PIceWirePayload = {
|
|
116
118
|
daemonId: string;
|
|
117
119
|
peerId?: string;
|
|
120
|
+
attemptId?: number;
|
|
121
|
+
attemptEpoch?: string;
|
|
118
122
|
candidate: string;
|
|
119
123
|
mid?: string;
|
|
120
124
|
sdpMid?: string | null;
|
|
@@ -154,16 +158,22 @@ export declare const isDashboardP2PSignalType: (value: unknown) => value is "p2p
|
|
|
154
158
|
export type DashboardP2PReadyWirePayload = {
|
|
155
159
|
daemonId: string;
|
|
156
160
|
peerId: string;
|
|
161
|
+
attemptId?: number;
|
|
162
|
+
attemptEpoch?: string;
|
|
157
163
|
};
|
|
158
164
|
export type DashboardP2PAnswerWirePayload = {
|
|
159
165
|
daemonId: string;
|
|
160
166
|
peerId: string;
|
|
161
167
|
sdp: string;
|
|
162
168
|
type: string;
|
|
169
|
+
attemptId?: number;
|
|
170
|
+
attemptEpoch?: string;
|
|
163
171
|
};
|
|
164
172
|
export type DashboardP2PLocalIceWirePayload = {
|
|
165
173
|
daemonId: string;
|
|
166
174
|
peerId: string;
|
|
175
|
+
attemptId?: number;
|
|
176
|
+
attemptEpoch?: string;
|
|
167
177
|
candidate: string;
|
|
168
178
|
sdpMid: string | null;
|
|
169
179
|
sdpMLineIndex: number | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Persisted role-launch intent. Restart replays this selection rather than
|
|
2
|
+
* consulting newer machine defaults; restore preserves its original provenance. */
|
|
3
|
+
import { type LaunchDefaultRole, type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
|
|
4
|
+
export interface RoleLaunchSelection {
|
|
5
|
+
role: LaunchDefaultRole;
|
|
6
|
+
selection: LaunchDefaultSelection;
|
|
7
|
+
applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
|
|
8
|
+
defaultRef?: LaunchDefaultRef;
|
|
9
|
+
documentRevision: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined;
|
package/dist/session-launch.d.ts
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* level, and WHERE each value came from.
|
|
4
|
-
*
|
|
5
|
-
* Wiring-unification Phase E (docs/design/2026-09-23-wiring-unification.md §7,
|
|
6
|
-
* RC5). Before this record the only trace of a session's model was a display
|
|
7
|
-
* string in recent-activity (`buildLegacyModelModeSummaryMetadata`), so nobody
|
|
8
|
-
* could answer "did I pick this model, did the dashboard restore it from last
|
|
9
|
-
* time, did a mesh slot choose it, or is it just the provider default?" — and
|
|
10
|
-
* after a daemon restart not even the value survived.
|
|
11
|
-
*
|
|
12
|
-
* The daemon owns the record (daemon-core `SessionRegistry`); this file holds the
|
|
13
|
-
* shape and the pure readers so daemon-core, web-core, the server and the MCP
|
|
14
|
-
* server all read one definition. It is a dependency-free leaf, like the rest of
|
|
15
|
-
* mesh-shared.
|
|
16
|
-
*
|
|
17
|
-
* ★Server content boundary (CLAUDE.md): the full record rides P2P only. The
|
|
18
|
-
* server path carries exactly two derived fields — `model` (an identifier,
|
|
19
|
-
* passed through {@link sanitizeModelIdentifier}) and `modelSource` (an enum,
|
|
20
|
-
* passed through {@link isModelAxisSource}). Never widen that to the record.
|
|
21
|
-
*/
|
|
22
|
-
export declare const MODEL_AXIS_SOURCES: readonly ["user", "remembered", "mesh_slot", "task_override", "provider_default", "unspecified"];
|
|
1
|
+
import { type RoleLaunchSelection } from './role-launch';
|
|
2
|
+
export declare const MODEL_AXIS_SOURCES: readonly ["user", "remembered", "stored_default", "project_default", "mesh_slot", "task_override", "provider_default", "unspecified"];
|
|
23
3
|
export type ModelAxisSource = typeof MODEL_AXIS_SOURCES[number];
|
|
24
4
|
export declare function isModelAxisSource(value: unknown): value is ModelAxisSource;
|
|
25
5
|
/**
|
|
@@ -65,6 +45,7 @@ export interface ModelSelection {
|
|
|
65
45
|
history: ModelSelectionHistoryEntry[];
|
|
66
46
|
}
|
|
67
47
|
export interface SessionLaunchRecord {
|
|
48
|
+
roleLaunch?: RoleLaunchSelection;
|
|
68
49
|
sessionId: string;
|
|
69
50
|
providerType: string;
|
|
70
51
|
providerVersion?: string;
|
package/dist/slot-proposal.d.ts
CHANGED
|
@@ -35,6 +35,13 @@ import { type MeshTaskDifficulty, type NodeCapabilitySlot } from './brain-routin
|
|
|
35
35
|
export interface CliSlotRecipe {
|
|
36
36
|
/** Optional model to pin on the slot. Best-effort at launch. */
|
|
37
37
|
model?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Optional ordered candidate models to prefer when the provider's live
|
|
40
|
+
* `modelOptions` (from model discovery) is present on `DetectedCliProvider`.
|
|
41
|
+
* The first candidate present in `modelOptions` wins; falls back to `model`
|
|
42
|
+
* when `modelOptions` is absent or contains none of the candidates.
|
|
43
|
+
*/
|
|
44
|
+
modelCandidates?: readonly string[];
|
|
38
45
|
/** Optional thinking level, in the provider's own vocabulary. */
|
|
39
46
|
thinkingLevel?: string;
|
|
40
47
|
/** Difficulty range this slot handles. Empty = general-purpose. */
|
|
@@ -76,6 +83,13 @@ export interface DetectedCliProvider {
|
|
|
76
83
|
displayName?: string;
|
|
77
84
|
/** Detected version, when known. Display only — never affects the mapping. */
|
|
78
85
|
version?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Live discovered / resolved `modelOptions` for this provider on the node,
|
|
88
|
+
* when available on the status catalog. When present and non-empty, recipes
|
|
89
|
+
* prefer the first matching model from `[recipe.model, ...recipe.modelCandidates]`
|
|
90
|
+
* that exists in `modelOptions`.
|
|
91
|
+
*/
|
|
92
|
+
modelOptions?: readonly string[];
|
|
79
93
|
}
|
|
80
94
|
/** One proposed slot plus why it was proposed. */
|
|
81
95
|
export interface ProposedSlotEntry {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* the protocol contract the marker demarcates.
|
|
31
31
|
*/
|
|
32
32
|
import { type MissionBrief } from './mission-brief';
|
|
33
|
-
export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff"];
|
|
33
|
+
export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "artifact_register"];
|
|
34
34
|
export type WorkerTool = typeof WORKER_TOOLS[number];
|
|
35
35
|
export declare function isWorkerTool(value: unknown): value is WorkerTool;
|
|
36
36
|
/** Stable marker line so a body is never footered twice and tests can find it. */
|
package/package.json
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
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 = [
|
|
@@ -33,6 +32,8 @@ export const ASSISTANT_TOOLS = [
|
|
|
33
32
|
'project_note',
|
|
34
33
|
'launch_defaults',
|
|
35
34
|
'launch_defaults_set',
|
|
35
|
+
'driver_list',
|
|
36
|
+
'driver_describe',
|
|
36
37
|
] as const
|
|
37
38
|
|
|
38
39
|
export type AssistantTool = typeof ASSISTANT_TOOLS[number]
|
|
@@ -46,6 +47,8 @@ export function isAssistantTool(value: unknown): value is AssistantTool {
|
|
|
46
47
|
/** Daemon verb names (§4.4 table + the five verbs below it). */
|
|
47
48
|
export const ASSISTANT_VERB = {
|
|
48
49
|
// tool verbs
|
|
50
|
+
driverList: 'driver_list',
|
|
51
|
+
driverDescribe: 'driver_describe',
|
|
49
52
|
projects: 'assistant_projects',
|
|
50
53
|
environment: 'assistant_environment',
|
|
51
54
|
action: 'assistant_action',
|
|
@@ -80,6 +83,8 @@ export type AssistantVerb = typeof ASSISTANT_VERB[keyof typeof ASSISTANT_VERB]
|
|
|
80
83
|
|
|
81
84
|
/** The daemon verb each assistant tool calls. */
|
|
82
85
|
export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>> = {
|
|
86
|
+
driver_list: ASSISTANT_VERB.driverList,
|
|
87
|
+
driver_describe: ASSISTANT_VERB.driverDescribe,
|
|
83
88
|
projects: ASSISTANT_VERB.projects,
|
|
84
89
|
environment: ASSISTANT_VERB.environment,
|
|
85
90
|
assistant_action: ASSISTANT_VERB.action,
|
|
@@ -123,8 +128,9 @@ export const ASSISTANT_REVIEW_TURN_VERBS: readonly AssistantVerb[] = [
|
|
|
123
128
|
ASSISTANT_VERB.projectNote,
|
|
124
129
|
]
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
import { canonicalDaemonId } from './daemon-normalize'
|
|
132
|
+
|
|
133
|
+
/** Env var the daemon sets on the assistant CLI's process (§4.5), read by the
|
|
128
134
|
* assistant MCP server and forwarded on every verb as
|
|
129
135
|
* `ASSISTANT_SESSION_ID_ARG`. Routing / ownership only — never an auth gate.
|
|
130
136
|
*/
|
|
@@ -132,3 +138,208 @@ export const ASSISTANT_SESSION_ID_ENV = 'ADHDEV_ASSISTANT_SESSION_ID'
|
|
|
132
138
|
|
|
133
139
|
/** Request arg carrying the calling assistant session id. */
|
|
134
140
|
export const ASSISTANT_SESSION_ID_ARG = 'assistantSessionId'
|
|
141
|
+
|
|
142
|
+
// ─── Remote Assistant Project Follow & Host-Authoritative Grant (rc24) ──────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Conceptual single-assistant slot in the verified account namespace.
|
|
146
|
+
* Fixed to `'personal'` — one logical assistant per account with one
|
|
147
|
+
* owner-selected active execution daemon binding per project ACL.
|
|
148
|
+
*/
|
|
149
|
+
export const ASSISTANT_ACCOUNT_SLOT = 'personal' as const
|
|
150
|
+
export type AssistantAccountSlot = typeof ASSISTANT_ACCOUNT_SLOT
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Daemon commands for remote project discovery, host ACL grant/revoke, and
|
|
154
|
+
* assistant-side follow/unfollow. Registered outside `ASSISTANT_VERB` so the
|
|
155
|
+
* assistant MCP server tool/verb parity table remains untouched.
|
|
156
|
+
*/
|
|
157
|
+
export const ASSISTANT_REMOTE_PROJECT_COMMAND = 'assistant_remote_project' as const
|
|
158
|
+
export const ASSISTANT_REMOTE_PROJECT_CATALOG_COMMAND = 'assistant_remote_project_catalog' as const
|
|
159
|
+
export const ASSISTANT_REMOTE_PROJECT_ACCESS_COMMAND = 'assistant_remote_project_access' as const
|
|
160
|
+
export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as const
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Narrow operations a non-roster follower deputy is permitted to invoke on
|
|
164
|
+
* `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
|
|
165
|
+
* `note`, `terminal`, and `inventory` remain strictly roster-member-only.
|
|
166
|
+
*/
|
|
167
|
+
export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send'] as const
|
|
168
|
+
export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
|
|
169
|
+
|
|
170
|
+
const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
|
|
171
|
+
|
|
172
|
+
export function isAssistantFollowerAllowedOp(value: unknown): value is AssistantFollowerAllowedOp {
|
|
173
|
+
return typeof value === 'string' && ASSISTANT_FOLLOWER_ALLOWED_OP_SET.has(value)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Host-authoritative narrow project ACL persisted on the hosting daemon's
|
|
178
|
+
* `LocalMeshEntry` in `meshes.json`. Revocation increments `grantRevision`
|
|
179
|
+
* and retains a `'revoked'` tombstone so stale follower caches cannot replay.
|
|
180
|
+
*/
|
|
181
|
+
export interface AssistantRemoteAccessBinding {
|
|
182
|
+
hostDaemonId: string
|
|
183
|
+
meshId: string
|
|
184
|
+
accountSlot: AssistantAccountSlot
|
|
185
|
+
activeDaemonId: string
|
|
186
|
+
bindingRevision: number
|
|
187
|
+
grantRevision: number
|
|
188
|
+
status: 'active' | 'revoked'
|
|
189
|
+
allowedOps: readonly AssistantFollowerAllowedOp[]
|
|
190
|
+
grantedAt: number
|
|
191
|
+
revokedAt?: number
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Durable owner-binding receipt persisted in the assistant daemon's
|
|
196
|
+
* `assistant.json`. Survives ordinary daemon restarts and session/provider
|
|
197
|
+
* replacements (`bindSession`), while missing/corrupt/mismatched receipts
|
|
198
|
+
* fail closed until owner reconciliation.
|
|
199
|
+
*/
|
|
200
|
+
export interface AssistantOwnerBindingReceipt {
|
|
201
|
+
accountSlot: AssistantAccountSlot
|
|
202
|
+
daemonId: string
|
|
203
|
+
bindingRevision: number
|
|
204
|
+
boundAt: number | string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type FollowedRemoteProjectState = 'followed' | 'unfollowed' | 'revoked'
|
|
208
|
+
|
|
209
|
+
export type RemoteAccessState =
|
|
210
|
+
| 'not_granted'
|
|
211
|
+
| 'granted_unfollowed'
|
|
212
|
+
| 'followed'
|
|
213
|
+
| 'revoked'
|
|
214
|
+
| 'host_unreachable'
|
|
215
|
+
| 'binding_mismatch'
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Assistant-local followed remote project reference persisted in `assistant.json`.
|
|
219
|
+
* Never injected into `meshes.json`, `mesh-host-records.json`, `mesh-node-state-push.json`,
|
|
220
|
+
* or `inlineMeshCache`.
|
|
221
|
+
*/
|
|
222
|
+
export interface FollowedRemoteProjectEntry {
|
|
223
|
+
/** Canonical key: `${canonicalDaemonId(hostDaemonId)}::${meshId}` */
|
|
224
|
+
key: string
|
|
225
|
+
hostDaemonId: string
|
|
226
|
+
hostLabel?: string
|
|
227
|
+
meshId: string
|
|
228
|
+
name: string
|
|
229
|
+
slug: string
|
|
230
|
+
sanitizedRepoIdentity: string | null
|
|
231
|
+
repoIdentity?: string
|
|
232
|
+
accountSlot: AssistantAccountSlot
|
|
233
|
+
bindingRevision: number
|
|
234
|
+
grantRevision: number
|
|
235
|
+
state: FollowedRemoteProjectState
|
|
236
|
+
followedAt: number | string
|
|
237
|
+
updatedAt: number | string
|
|
238
|
+
unfollowedAt?: number | string
|
|
239
|
+
revokedAt?: number | string
|
|
240
|
+
lastCursor?: string | null
|
|
241
|
+
coordinatorSessionId?: string | null
|
|
242
|
+
lastErrorCode?: string
|
|
243
|
+
lastObserved?: {
|
|
244
|
+
observedAt: number | string
|
|
245
|
+
coordinatorState?: string
|
|
246
|
+
coordinatorMissing?: boolean
|
|
247
|
+
coordinatorPresent?: boolean
|
|
248
|
+
lastHeadline?: string
|
|
249
|
+
summary?: string
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Content-free canonical catalog metadata returned by `assistant_remote_project_catalog`
|
|
255
|
+
* for meshes actually hosted on the queried remote daemon.
|
|
256
|
+
*/
|
|
257
|
+
export interface RemoteProjectCatalogItem {
|
|
258
|
+
hostDaemonId: string
|
|
259
|
+
meshId: string
|
|
260
|
+
name: string
|
|
261
|
+
sanitizedRepoIdentity: string | null
|
|
262
|
+
coordinatorState?: 'none' | 'idle' | 'working' | 'waiting' | string
|
|
263
|
+
capabilities: {
|
|
264
|
+
status: true
|
|
265
|
+
read: true
|
|
266
|
+
poll: true
|
|
267
|
+
report: true
|
|
268
|
+
send: true
|
|
269
|
+
coordinatorPresent?: boolean
|
|
270
|
+
}
|
|
271
|
+
observedAt: number | string
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Build the canonical `(hostDaemonId, meshId)` key used across assistant follow state. */
|
|
275
|
+
export function followedRemoteProjectKey(hostDaemonId: string, meshId: string): string {
|
|
276
|
+
const canonHost = canonicalDaemonId(hostDaemonId) ?? hostDaemonId.trim()
|
|
277
|
+
return `${canonHost}::${meshId.trim()}`
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Sanitize a repository identity or remote URL into a content-free `host/path`
|
|
282
|
+
* identifier suitable for remote discovery catalogs. Strips credentials, query
|
|
283
|
+
* strings, fragments, and `.git` suffixes, and rejects local filesystem paths.
|
|
284
|
+
*/
|
|
285
|
+
export function sanitizeRemoteRepoIdentity(raw: string | null | undefined): string | null {
|
|
286
|
+
if (typeof raw !== 'string') return null
|
|
287
|
+
const trimmed = raw.trim()
|
|
288
|
+
if (!trimmed) return null
|
|
289
|
+
|
|
290
|
+
// Reject local filesystem paths (Unix absolute/relative/home, Windows drive/UNC, file:// URLs).
|
|
291
|
+
if (
|
|
292
|
+
trimmed.startsWith('/')
|
|
293
|
+
|| trimmed.startsWith('./')
|
|
294
|
+
|| trimmed.startsWith('../')
|
|
295
|
+
|| trimmed.startsWith('~/')
|
|
296
|
+
|| trimmed.startsWith('\\')
|
|
297
|
+
|| /^[a-zA-Z]:[\\/]/.test(trimmed)
|
|
298
|
+
|| /^file:/i.test(trimmed)
|
|
299
|
+
) {
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const withoutQuery = trimmed.replace(/[?#].*$/, '').replace(/\/+$/, '')
|
|
304
|
+
if (!withoutQuery) return null
|
|
305
|
+
|
|
306
|
+
let host = ''
|
|
307
|
+
let repoPath = ''
|
|
308
|
+
|
|
309
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(withoutQuery)) {
|
|
310
|
+
try {
|
|
311
|
+
const url = new URL(withoutQuery)
|
|
312
|
+
if (url.protocol === 'file:') return null
|
|
313
|
+
host = url.hostname.toLowerCase()
|
|
314
|
+
repoPath = decodeURIComponent(url.pathname)
|
|
315
|
+
.replace(/^\/+|\/+$/g, '')
|
|
316
|
+
.replace(/\.git$/i, '')
|
|
317
|
+
} catch {
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
const scpMatch = withoutQuery.match(/^(?:[^@/:]+@)?(\[[^\]]+\]|[^/:]+):(.+)$/)
|
|
322
|
+
if (scpMatch) {
|
|
323
|
+
host = scpMatch[1].replace(/^\[|\]$/g, '').toLowerCase()
|
|
324
|
+
repoPath = scpMatch[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
|
|
325
|
+
} else {
|
|
326
|
+
const slash = withoutQuery.indexOf('/')
|
|
327
|
+
if (slash > 0) {
|
|
328
|
+
const rawHost = withoutQuery.slice(0, slash)
|
|
329
|
+
const atIdx = rawHost.lastIndexOf('@')
|
|
330
|
+
host = (atIdx >= 0 ? rawHost.slice(atIdx + 1) : rawHost).toLowerCase()
|
|
331
|
+
repoPath = withoutQuery.slice(slash + 1).replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!host || !repoPath) return null
|
|
337
|
+
if (!host.includes('.') && host !== 'localhost') return null
|
|
338
|
+
if (/[\s@:\\]/.test(host)) return null
|
|
339
|
+
const segments = repoPath.split('/').filter(Boolean)
|
|
340
|
+
if (segments.length === 0) return null
|
|
341
|
+
if (segments.some(seg => seg === '.' || seg === '..' || /[\s@:\\]/.test(seg))) return null
|
|
342
|
+
|
|
343
|
+
return `${host}/${segments.join('/')}`.slice(0, 200)
|
|
344
|
+
}
|
|
345
|
+
|
package/src/index.ts
CHANGED
package/src/launch-defaults.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface LaunchDefaultAxis {
|
|
|
57
57
|
export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
|
|
58
58
|
export type LaunchDefaultValidationReason =
|
|
59
59
|
| 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled'
|
|
60
|
-
| 'provider_not_detected' | 'provider_not_eligible' | 'model_unsupported'
|
|
60
|
+
| 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported'
|
|
61
61
|
| 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported'
|
|
62
62
|
| 'thinking_not_offered' | 'catalog_unavailable';
|
|
63
63
|
export type LaunchDefaultResolution =
|
package/src/mesh-tool-names.ts
CHANGED
|
@@ -65,6 +65,9 @@ export const CANONICAL_MESH_TOOL_NAMES = [
|
|
|
65
65
|
'mesh_mission_list',
|
|
66
66
|
'mesh_review_inbox',
|
|
67
67
|
'mesh_node_slots',
|
|
68
|
+
'driver_list',
|
|
69
|
+
'driver_describe',
|
|
70
|
+
'artifact_list',
|
|
68
71
|
] as const;
|
|
69
72
|
|
|
70
73
|
export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
|
package/src/outbound-message.ts
CHANGED
|
@@ -321,3 +321,20 @@ export function isOutboundMessage(value: unknown): value is OutboundMessage {
|
|
|
321
321
|
&& isSendPolicy(candidate.policy)
|
|
322
322
|
&& typeof candidate.createdAt === 'number'
|
|
323
323
|
}
|
|
324
|
+
|
|
325
|
+
const PASTED_OPEN_RE = /^\s*<pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
|
|
326
|
+
const PASTED_CLOSE_RE = /^\s*<\/pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Drop the `<pasted_content>` envelope lines Claude Code wraps around a pasted
|
|
330
|
+
* input. Only a text that OPENS with the envelope is touched; anything else is
|
|
331
|
+
* returned unchanged. Shared across daemon transcript merge and web echo/display
|
|
332
|
+
* normalization so wrapped turns correlate identically.
|
|
333
|
+
*/
|
|
334
|
+
export function unwrapPastedContent(text: string): string {
|
|
335
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n')
|
|
336
|
+
const first = lines.findIndex((line) => line.trim())
|
|
337
|
+
if (first < 0 || !PASTED_OPEN_RE.test(lines[first])) return text
|
|
338
|
+
return lines.filter((line) => !PASTED_OPEN_RE.test(line) && !PASTED_CLOSE_RE.test(line)).join('\n')
|
|
339
|
+
}
|
|
340
|
+
|
|
@@ -260,8 +260,8 @@ export const DAEMON_P2P_SIGNAL_TYPES = ['p2p_offer', 'p2p_ice'] as const
|
|
|
260
260
|
export type DaemonP2PSignalType = typeof DAEMON_P2P_SIGNAL_TYPES[number]
|
|
261
261
|
export const isDaemonP2PSignalType = makeTypeGuard(DAEMON_P2P_SIGNAL_TYPES)
|
|
262
262
|
|
|
263
|
-
export type P2POfferWirePayload = { sdp: string; type: string; peerId: string }
|
|
264
|
-
export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string }
|
|
263
|
+
export type P2POfferWirePayload = { sdp: string; type: string; peerId: string; attemptId?: number; attemptEpoch?: string }
|
|
264
|
+
export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string; attemptId?: number; attemptEpoch?: string }
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
267
|
* A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
|
|
@@ -273,6 +273,8 @@ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string
|
|
|
273
273
|
export type RelayedP2PSignalWirePayload = {
|
|
274
274
|
peerId?: string
|
|
275
275
|
daemonId?: string
|
|
276
|
+
attemptId?: number
|
|
277
|
+
attemptEpoch?: string
|
|
276
278
|
sharePermission?: string
|
|
277
279
|
shareTargetSessionId?: string
|
|
278
280
|
sdp?: string
|
|
@@ -152,6 +152,8 @@ export type SystemAnnouncementWirePayload = {
|
|
|
152
152
|
export type DashboardP2POfferWirePayload = {
|
|
153
153
|
daemonId: string
|
|
154
154
|
peerId?: string
|
|
155
|
+
attemptId?: number
|
|
156
|
+
attemptEpoch?: string
|
|
155
157
|
sdp: string
|
|
156
158
|
type: string
|
|
157
159
|
}
|
|
@@ -159,6 +161,8 @@ export type DashboardP2POfferWirePayload = {
|
|
|
159
161
|
export type DashboardP2PIceWirePayload = {
|
|
160
162
|
daemonId: string
|
|
161
163
|
peerId?: string
|
|
164
|
+
attemptId?: number
|
|
165
|
+
attemptEpoch?: string
|
|
162
166
|
candidate: string
|
|
163
167
|
mid?: string
|
|
164
168
|
sdpMid?: string | null
|
|
@@ -234,11 +238,13 @@ export const DASHBOARD_P2P_SIGNAL_TYPES = ['p2p_ready', 'p2p_answer', 'p2p_ice']
|
|
|
234
238
|
export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number]
|
|
235
239
|
export const isDashboardP2PSignalType = makeTypeGuard(DASHBOARD_P2P_SIGNAL_TYPES)
|
|
236
240
|
|
|
237
|
-
export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string }
|
|
238
|
-
export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string }
|
|
241
|
+
export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string; attemptId?: number; attemptEpoch?: string }
|
|
242
|
+
export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string; attemptId?: number; attemptEpoch?: string }
|
|
239
243
|
export type DashboardP2PLocalIceWirePayload = {
|
|
240
244
|
daemonId: string
|
|
241
245
|
peerId: string
|
|
246
|
+
attemptId?: number
|
|
247
|
+
attemptEpoch?: string
|
|
242
248
|
candidate: string
|
|
243
249
|
sdpMid: string | null
|
|
244
250
|
sdpMLineIndex: number | null
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Persisted role-launch intent. Restart replays this selection rather than
|
|
2
|
+
* consulting newer machine defaults; restore preserves its original provenance. */
|
|
3
|
+
import { isLaunchDefaultRole, isLaunchDefaultSource, type LaunchDefaultRole,
|
|
4
|
+
type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
|
|
5
|
+
export interface RoleLaunchSelection {
|
|
6
|
+
role: LaunchDefaultRole;
|
|
7
|
+
selection: LaunchDefaultSelection;
|
|
8
|
+
applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
|
|
9
|
+
defaultRef?: LaunchDefaultRef;
|
|
10
|
+
documentRevision: number;
|
|
11
|
+
}
|
|
12
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
14
|
+
? value as Record<string, unknown> : undefined;
|
|
15
|
+
}
|
|
16
|
+
function ref(value: unknown): LaunchDefaultRef | undefined {
|
|
17
|
+
const r = record(value);
|
|
18
|
+
if (!r || !isLaunchDefaultRole(r.role) || (r.scope !== 'global' && r.scope !== 'project')
|
|
19
|
+
|| !Number.isSafeInteger(r.revision) || (r.revision as number) < 1
|
|
20
|
+
|| (r.scope === 'project' && (typeof r.projectId !== 'string' || !r.projectId))) return undefined;
|
|
21
|
+
return { role: r.role, scope: r.scope, revision: r.revision as number,
|
|
22
|
+
...(r.scope === 'project' ? { projectId: r.projectId as string } : {}) };
|
|
23
|
+
}
|
|
24
|
+
function axis(value: unknown): LaunchDefaultAxis | undefined {
|
|
25
|
+
const a = record(value);
|
|
26
|
+
if (!a || !isLaunchDefaultSource(a.source) || (a.value !== null && typeof a.value !== 'string')) return undefined;
|
|
27
|
+
const defaultRef = ref(a.defaultRef);
|
|
28
|
+
if (a.source === 'stored_default' && defaultRef?.scope !== 'global'
|
|
29
|
+
|| a.source === 'project_default' && defaultRef?.scope !== 'project'
|
|
30
|
+
|| (a.source === 'user' || a.source === 'provider_default') && a.defaultRef !== undefined) return undefined;
|
|
31
|
+
return { value: a.value as string | null, source: a.source, ...(defaultRef ? { defaultRef } : {}) };
|
|
32
|
+
}
|
|
33
|
+
export function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined {
|
|
34
|
+
const r = record(value), s = record(r?.selection), a = record(r?.applied);
|
|
35
|
+
if (!r || !isLaunchDefaultRole(r.role) || !s || typeof s.provider !== 'string' || !s.provider
|
|
36
|
+
|| !Number.isSafeInteger(r.documentRevision) || (r.documentRevision as number) < 1
|
|
37
|
+
|| (s.model !== undefined && typeof s.model !== 'string')
|
|
38
|
+
|| (s.thinkingLevel !== undefined && typeof s.thinkingLevel !== 'string')) return undefined;
|
|
39
|
+
const provider = axis(a?.provider), model = axis(a?.model), thinkingLevel = axis(a?.thinkingLevel);
|
|
40
|
+
if (!provider || !model || !thinkingLevel
|
|
41
|
+
|| [provider, model, thinkingLevel].some(axis => axis.defaultRef && (axis.defaultRef.role !== r.role
|
|
42
|
+
|| axis.defaultRef.scope === 'global' && axis.defaultRef.revision > (r.documentRevision as number)))
|
|
43
|
+
|| provider.value !== s.provider
|
|
44
|
+
|| model.value !== (s.model ?? null) || thinkingLevel.value !== (s.thinkingLevel ?? null)) return undefined;
|
|
45
|
+
const defaultRef = ref(r.defaultRef);
|
|
46
|
+
return { role: r.role, selection: { provider: s.provider,
|
|
47
|
+
...(typeof s.model === 'string' ? { model: s.model } : {}),
|
|
48
|
+
...(typeof s.thinkingLevel === 'string' ? { thinkingLevel: s.thinkingLevel } : {}) },
|
|
49
|
+
applied: { provider, model, thinkingLevel }, ...(defaultRef ? { defaultRef } : {}),
|
|
50
|
+
documentRevision: r.documentRevision as number };
|
|
51
|
+
}
|