dsh-home-hosted 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/docs/DESIGN.md +141 -13
- package/lib/client.js +6 -6
- package/lib/index.js +1105 -270
- package/lib/index.js.map +4 -4
- package/lib/types/config.d.ts +6 -0
- package/lib/types/home-hosted/dsh-entry.d.ts +14 -8
- package/lib/types/home-hosted/entries.d.ts +4 -18
- package/lib/types/home-hosted/instances-notice.d.ts +35 -0
- package/lib/types/home-hosted/instances.d.ts +34 -0
- package/lib/types/home-hosted/launcher.d.ts +20 -1
- package/lib/types/home-hosted/panel-control.d.ts +14 -0
- package/lib/types/home-hosted/resolve.d.ts +1 -5
- package/lib/types/home-hosted/token.d.ts +8 -3
- package/lib/types/service.d.ts +75 -12
- package/lib/types/shared/contracts.d.ts +92 -11
- package/lib/types/util/paths.d.ts +21 -0
- package/package.json +2 -2
package/lib/types/config.d.ts
CHANGED
|
@@ -11,5 +11,11 @@ export interface Config {
|
|
|
11
11
|
homeHostedCommand?: string;
|
|
12
12
|
/** The entry this plugin manages for the running harness. */
|
|
13
13
|
defaultEntryId: string;
|
|
14
|
+
/**
|
|
15
|
+
* Extra home-hosted state roots to report as other panels. Discovery already
|
|
16
|
+
* knows the managed root, `$HHOSTED_HOME` and the `~/.home-hosted*` siblings;
|
|
17
|
+
* a panel run from a project with its own `--home` is declared here.
|
|
18
|
+
*/
|
|
19
|
+
instanceRoots: string[];
|
|
14
20
|
}
|
|
15
21
|
export declare const Config: z<Config>;
|
|
@@ -12,7 +12,6 @@ export declare function detectProfile(argv: readonly string[], env?: Record<stri
|
|
|
12
12
|
export interface DshFacts {
|
|
13
13
|
id: string;
|
|
14
14
|
port: number | null;
|
|
15
|
-
host: string;
|
|
16
15
|
profile: string | null;
|
|
17
16
|
dshHome: string;
|
|
18
17
|
launch: CliLaunch | null;
|
|
@@ -57,19 +56,26 @@ export declare function findDshEntry(dshHome: string): string | null;
|
|
|
57
56
|
*/
|
|
58
57
|
export declare function resolveDshLaunch(options?: ResolveDshLaunchOptions): Promise<DshLaunch | null>;
|
|
59
58
|
/**
|
|
60
|
-
* Re-point an entry
|
|
59
|
+
* Re-point an entry at the stable launcher for the pinned image.
|
|
61
60
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
61
|
+
* Qualifying shapes, and only these: the entry runs the script this pattern
|
|
62
|
+
* pins (a local build recorded in an earlier release), reaches that script
|
|
63
|
+
* through a package-manager shim, or names a bare `dsh`/`dsh.cmd` that a later
|
|
64
|
+
* PATH lookup would answer with a different copy. An entry that runs a
|
|
65
|
+
* *different* dsh is somebody's deliberate choice and is never touched.
|
|
66
|
+
*
|
|
67
|
+
* An entry this process was booted through is also left alone: repairing it
|
|
68
|
+
* would point it at itself, since that is already the launcher.
|
|
66
69
|
*/
|
|
70
|
+
export declare function pinnedScriptFor(launch: DshLaunch | null): string | null;
|
|
67
71
|
/**
|
|
68
72
|
* Whether an entry *could* need that repair, without resolving anything.
|
|
69
73
|
*
|
|
70
74
|
* Resolving a local dsh walks the harness home, so the write path asks this
|
|
71
|
-
* first
|
|
72
|
-
*
|
|
75
|
+
* first. Almost every shape can need one — an absolute command, an absolute
|
|
76
|
+
* first argument, a bare `dsh` — and the cheap answer is therefore "yes" here
|
|
77
|
+
* and a full comparison in `launcherRepair`, which is what decides whether
|
|
78
|
+
* anything is actually rewritten.
|
|
73
79
|
*/
|
|
74
80
|
export declare function needsLauncherRepair(config: {
|
|
75
81
|
command?: string;
|
|
@@ -12,24 +12,10 @@ import type { EntryIntent, OnPortConflict, ServerEntry, ServerEntryPatch } from
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function defaultOnPortConflict(platform?: NodeJS.Platform): OnPortConflict;
|
|
14
14
|
export declare function defaultIntent(id: string, platform?: NodeJS.Platform): EntryIntent;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* key: an older schema drops it and warns, so writing one is noise at best.
|
|
20
|
-
*/
|
|
21
|
-
export declare function ownedPatch(intent: EntryIntent, live?: ServerEntry | null, options?: {
|
|
22
|
-
persistent?: boolean;
|
|
23
|
-
}): ServerEntryPatch;
|
|
24
|
-
/**
|
|
25
|
-
* Owned keys whose live value differs from the intent.
|
|
26
|
-
*
|
|
27
|
-
* The intent keeps what the person asked for; a key the panel cannot parse is
|
|
28
|
-
* not drift, or every entry would report one on an older panel.
|
|
29
|
-
*/
|
|
30
|
-
export declare function ownedDrift(live: ServerEntry | null, intent: EntryIntent, options?: {
|
|
31
|
-
persistent?: boolean;
|
|
32
|
-
}): string[];
|
|
15
|
+
/** The owned keys as a patch, preserving any sibling keys inside `stop`. */
|
|
16
|
+
export declare function ownedPatch(intent: EntryIntent, live?: ServerEntry | null): ServerEntryPatch;
|
|
17
|
+
/** Owned keys whose live value differs from the intent. */
|
|
18
|
+
export declare function ownedDrift(live: ServerEntry | null, intent: EntryIntent): string[];
|
|
33
19
|
/**
|
|
34
20
|
* Only the owned keys, as they were before this plugin touched the entry.
|
|
35
21
|
*
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling the agent which home-hosted panels exist.
|
|
3
|
+
*
|
|
4
|
+
* A plugin that owns one panel must not look like the only one on the machine.
|
|
5
|
+
* When discovery finds more than one, the inventory is contributed as dynamic
|
|
6
|
+
* runtime context (`ctx.systemPrompt.context`), so every model step knows the
|
|
7
|
+
* managed panel, the others, and that this plugin cannot write to them.
|
|
8
|
+
*
|
|
9
|
+
* The contribution reads the service's cached inventory: assembly happens
|
|
10
|
+
* before a model step and must never block on the disk.
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import type { InstanceView } from '../shared/contracts.js';
|
|
14
|
+
/** The subset of `ctx.systemPrompt` this uses; absent on a host without it. */
|
|
15
|
+
export interface SystemPromptLike {
|
|
16
|
+
context: (contribution: {
|
|
17
|
+
name: string;
|
|
18
|
+
order: number;
|
|
19
|
+
text: string | ((context: unknown) => string);
|
|
20
|
+
}) => () => void;
|
|
21
|
+
}
|
|
22
|
+
export interface InstancesNoticeDeps {
|
|
23
|
+
/** The inventory the plugin already measured; never a disk read at assembly. */
|
|
24
|
+
instances: () => readonly InstanceView[];
|
|
25
|
+
/** The `instancesNotice` setting, read per assembly. */
|
|
26
|
+
enabled: () => boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* After the first-party runtime contexts (sandbox 110, approval 115,
|
|
30
|
+
* subagent delegation 120) and before anything a repository owns.
|
|
31
|
+
*/
|
|
32
|
+
export declare const INSTANCES_CONTEXT_ORDER = 130;
|
|
33
|
+
/** The exact text one assembly contributes, or `''` for nothing. */
|
|
34
|
+
export declare function instancesContextText(instances: readonly InstanceView[], enabled: boolean): string;
|
|
35
|
+
export declare function registerInstancesNotice(ctx: Context, deps: InstancesNoticeDeps): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { InstanceView } from '../shared/contracts.js';
|
|
2
|
+
export interface InstanceDiscoveryOptions {
|
|
3
|
+
/** The state root this plugin drives. */
|
|
4
|
+
managedHome: string;
|
|
5
|
+
/** Extra roots an operator declared in the plugin row. */
|
|
6
|
+
extraRoots?: readonly string[];
|
|
7
|
+
/** Test seam: this process's `$HHOSTED_HOME`. */
|
|
8
|
+
envHome?: string | null;
|
|
9
|
+
/** Test seam: `HHOSTED_SERVER_ID`, or null when the panel does not supervise us. */
|
|
10
|
+
hostingEntryId?: string | null;
|
|
11
|
+
/** Test seam: the user's home directory. */
|
|
12
|
+
homeDir?: string;
|
|
13
|
+
/** Test seam: directory listing. */
|
|
14
|
+
listDir?: (dir: string) => string[] | null;
|
|
15
|
+
/** Test seam: whether a pid is alive. */
|
|
16
|
+
alive?: (pid: number) => boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A path as one identity: `~` expanded, symlinks resolved (macOS `/var`). */
|
|
19
|
+
export declare function canonicalPath(value: string): string;
|
|
20
|
+
/** Every panel found, the managed one first and the rest by state root. */
|
|
21
|
+
export declare function discoverInstances(options: InstanceDiscoveryOptions): InstanceView[];
|
|
22
|
+
/**
|
|
23
|
+
* One line naming a panel, for a prompt note or a refusal. A root whose
|
|
24
|
+
* `run.json` is gone has no runtime to report — home-hosted drops a stale one —
|
|
25
|
+
* so it is never called "stopped".
|
|
26
|
+
*/
|
|
27
|
+
export declare function describeInstance(instance: InstanceView): string;
|
|
28
|
+
/**
|
|
29
|
+
* The note the agent is given when the machine holds more than one panel.
|
|
30
|
+
*
|
|
31
|
+
* `null` for a single panel: the plugin's preference is only worth stating when
|
|
32
|
+
* there is something to prefer it over.
|
|
33
|
+
*/
|
|
34
|
+
export declare function instancesNoticeText(instances: readonly InstanceView[]): string | null;
|
|
@@ -40,6 +40,18 @@ export interface DshLauncherOptions {
|
|
|
40
40
|
dshHome: string;
|
|
41
41
|
/** Where the dsh entry is right now; recorded as the fast path. */
|
|
42
42
|
resolvedEntry: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The image this plugin was installed on: the dsh a boot entry must keep
|
|
45
|
+
* running. Recorded and checked before anything else, so a copy that merely
|
|
46
|
+
* reports a higher version never wins the boot.
|
|
47
|
+
*/
|
|
48
|
+
pinnedEntry?: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Adopt `pinnedEntry` even when the record already pins something else. Set
|
|
51
|
+
* only where the caller *is* the running image; a process booted through the
|
|
52
|
+
* launcher must carry the pin it started under instead of replacing it.
|
|
53
|
+
*/
|
|
54
|
+
repin?: boolean;
|
|
43
55
|
/** The extension the launcher must match: a `.cjs` entry gets a `.cjs` launcher. */
|
|
44
56
|
entryExtension?: string | null;
|
|
45
57
|
/**
|
|
@@ -53,8 +65,15 @@ export interface DshLauncherWrite {
|
|
|
53
65
|
path: string;
|
|
54
66
|
changed: boolean;
|
|
55
67
|
}
|
|
56
|
-
interface DshLauncherRecord {
|
|
68
|
+
export interface DshLauncherRecord {
|
|
57
69
|
entry: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* The running image, while it still exists. A path here is an instruction:
|
|
72
|
+
* `choose()` runs this one and never sorts it against another copy.
|
|
73
|
+
*/
|
|
74
|
+
pinned?: string | null;
|
|
75
|
+
/** When that image was last confirmed. */
|
|
76
|
+
pinnedAt?: number | null;
|
|
58
77
|
/** Where that entry's install sat, kept across rewrites while it stays valid. */
|
|
59
78
|
roots: string[];
|
|
60
79
|
writtenAt: number;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ChildProcess } from 'node:child_process';
|
|
2
2
|
import type { PanelControlResult } from '../shared/contracts.js';
|
|
3
3
|
import { run } from '../util/exec.js';
|
|
4
|
+
import type { RunResult } from '../util/exec.js';
|
|
4
5
|
import type { CliLaunch } from './launch.js';
|
|
6
|
+
/** How the panel's CLI is run: the resolved argv plus an environment. */
|
|
7
|
+
export type PanelCliExec = (args: string[], env: Record<string, string | undefined>) => Promise<RunResult>;
|
|
5
8
|
export interface PanelControlDeps {
|
|
6
9
|
/** The CLI to run, already resolved. */
|
|
7
10
|
launch: CliLaunch | null;
|
|
@@ -11,9 +14,20 @@ export interface PanelControlDeps {
|
|
|
11
14
|
/** Environment for the spawned CLI (absolute PATH, HOME, HHOSTED_HOME). */
|
|
12
15
|
env: Record<string, string>;
|
|
13
16
|
timeoutMs?: number;
|
|
17
|
+
/** Test seam: run the resolved CLI without spawning it. */
|
|
18
|
+
exec?: PanelCliExec;
|
|
14
19
|
}
|
|
15
20
|
/** Start the preferred CLI as a detached panel; the CLI itself does the detaching. */
|
|
16
21
|
export declare function startPanel(deps: PanelControlDeps): Promise<PanelControlResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Stop the panel through its own CLI.
|
|
24
|
+
*
|
|
25
|
+
* The panel is the parent of every server it supervises, so this stops those
|
|
26
|
+
* too — including the dsh this plugin may be running in. That is the point of
|
|
27
|
+
* the button, not a hazard to hide: the caller's UI says so. An entry with
|
|
28
|
+
* `autostart` is started again on the next panel start; it is not restarted here.
|
|
29
|
+
*/
|
|
30
|
+
export declare function stopPanel(deps: PanelControlDeps): Promise<PanelControlResult>;
|
|
17
31
|
export declare function takeoverHelperPath(stateDir: string): string;
|
|
18
32
|
export declare function takeoverLogPath(stateDir: string): string;
|
|
19
33
|
/** The generated helper's source; exported so a test can inspect it. */
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import type { CliStatus } from '../shared/contracts.js';
|
|
2
2
|
import type { CliLaunch } from './launch.js';
|
|
3
3
|
/** The range the plugin ships and is tested against. */
|
|
4
|
-
export declare const EXPECTED_RANGE = "^0.6.
|
|
4
|
+
export declare const EXPECTED_RANGE = "^0.6.7";
|
|
5
5
|
/** Oldest release whose API and config schema this plugin relies on. */
|
|
6
6
|
export declare const MIN_SUPPORTED_VERSION = "0.4.1";
|
|
7
|
-
/** Oldest release whose config schema accepts `onPortConflict: kill`. */
|
|
8
|
-
export declare const MIN_KILL_VERSION = "0.6.0";
|
|
9
|
-
/** `persistent` (the nanny that keeps an entry alive across panel restarts). */
|
|
10
|
-
export declare const MIN_PERSISTENT_VERSION = "0.6.3";
|
|
11
7
|
export type CliPreference = 'pinned' | 'global';
|
|
12
8
|
export interface CliResolution {
|
|
13
9
|
launch: CliLaunch | null;
|
|
@@ -4,6 +4,8 @@ export interface EnsureTokenOptions {
|
|
|
4
4
|
home: string;
|
|
5
5
|
stateDir: string;
|
|
6
6
|
exec: CliExecutor;
|
|
7
|
+
/** Which panel this credential is for; omitted is the panel the plugin manages. */
|
|
8
|
+
slot?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface EnsureTokenResult {
|
|
9
11
|
token: string | null;
|
|
@@ -15,14 +17,17 @@ export interface ReclaimTokenOptions {
|
|
|
15
17
|
home: string;
|
|
16
18
|
stateDir: string;
|
|
17
19
|
exec: CliExecutor;
|
|
20
|
+
slot?: string;
|
|
18
21
|
}
|
|
19
22
|
export interface ReclaimTokenResult {
|
|
20
23
|
token: string | null;
|
|
21
24
|
detail: string;
|
|
22
25
|
}
|
|
23
|
-
|
|
24
|
-
export declare function
|
|
25
|
-
export declare function
|
|
26
|
+
/** A stable, unguessable file name for one state root's credential. */
|
|
27
|
+
export declare function tokenSlot(home: string): string;
|
|
28
|
+
export declare function storedTokenPath(stateDir: string, slot?: string): string;
|
|
29
|
+
export declare function readStoredToken(stateDir: string, slot?: string): string | null;
|
|
30
|
+
export declare function storeToken(stateDir: string, token: string, slot?: string): void;
|
|
26
31
|
export declare function generateToken(): string;
|
|
27
32
|
/** Whether home-hosted already holds an API token (only its hash is on disk). */
|
|
28
33
|
export declare function apiTokenEnrolled(home: string): boolean;
|
package/lib/types/service.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* only when the panel is not answering.
|
|
8
8
|
*/
|
|
9
9
|
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
10
|
-
import type { BootMechanism, BootStatus, HomeHostedStatus, ManagedEntryStatus, PanelControlResult, PanelStatus, RpcEndpoint, UiAction, UiResult } from './shared/contracts.js';
|
|
10
|
+
import type { BootMechanism, BootStatus, HomeHostedStatus, InstanceView, ManagedEntryStatus, PanelControlResult, PanelStatus, RpcEndpoint, UiAction, UiResult } from './shared/contracts.js';
|
|
11
11
|
import type { BootSpec } from './boot/types.js';
|
|
12
12
|
import type { DshLaunch } from './home-hosted/dsh-entry.js';
|
|
13
13
|
import type { SettingsStore } from './settings.js';
|
|
@@ -35,9 +35,17 @@ export declare class HomeHostedError extends Error {
|
|
|
35
35
|
}
|
|
36
36
|
export interface HomeHostedServiceOptions {
|
|
37
37
|
home: string;
|
|
38
|
+
/** How that root was chosen: an explicit env var, an adopted legacy panel, or this instance's own. */
|
|
39
|
+
panelHomeSource?: 'env' | 'legacy' | 'instance';
|
|
38
40
|
stateDir: string;
|
|
39
41
|
homeHostedCommand?: string;
|
|
40
42
|
defaultEntryId: string;
|
|
43
|
+
/** Other home-hosted state roots to report as panels; discovery finds the rest. */
|
|
44
|
+
instanceRoots?: readonly string[];
|
|
45
|
+
/** Test seam: this process's `$HHOSTED_HOME`; defaults to the environment. */
|
|
46
|
+
envHome?: string | null;
|
|
47
|
+
/** Test seam: where `~/.home-hosted*` siblings are looked for; defaults to the user's home. */
|
|
48
|
+
homeDir?: string;
|
|
41
49
|
settings: SettingsStore;
|
|
42
50
|
/** Test seam: run the home-hosted CLI without spawning it. */
|
|
43
51
|
execCli?: (args: string[], env: Record<string, string | undefined>) => Promise<RunResult>;
|
|
@@ -57,9 +65,64 @@ export declare class HomeHostedService extends Service {
|
|
|
57
65
|
private tokenDetail;
|
|
58
66
|
/** When a missing managed entry was last put back, so a poll cannot become a write loop. */
|
|
59
67
|
private entryRecoveryAt;
|
|
68
|
+
/** The last panel inventory, so a model-step assembly never reads the disk. */
|
|
69
|
+
private instanceCache;
|
|
60
70
|
private readonly snapshotsFile;
|
|
61
71
|
constructor(ctx: Context, options: HomeHostedServiceOptions);
|
|
62
72
|
private resolveDsh;
|
|
73
|
+
/**
|
|
74
|
+
* The panel inventory, cached for a short while: the page polls status and
|
|
75
|
+
* the prompt provider reads it at every assembly. `refresh` (or an expired
|
|
76
|
+
* cache) re-reads the disk.
|
|
77
|
+
*/
|
|
78
|
+
instances(refresh?: boolean): Promise<InstanceView[]>;
|
|
79
|
+
/**
|
|
80
|
+
* The inventory for a caller that cannot await — a prompt provider runs
|
|
81
|
+
* synchronously before a model step. Discovery is synchronous too, so this
|
|
82
|
+
* re-measures a stale cache in place and never blocks the step.
|
|
83
|
+
*/
|
|
84
|
+
instancesNow(): InstanceView[];
|
|
85
|
+
private measureInstances;
|
|
86
|
+
/** The inventory as last measured, without touching the disk. */
|
|
87
|
+
instancesSnapshot(): InstanceView[];
|
|
88
|
+
/**
|
|
89
|
+
* The state root a call targets: the managed one unless it names another panel
|
|
90
|
+
* discovery reported. A caller-supplied path is never acted on directly, so a
|
|
91
|
+
* stray string cannot become a way to write anywhere on the disk.
|
|
92
|
+
*/
|
|
93
|
+
private targetHome;
|
|
94
|
+
/**
|
|
95
|
+
* How a call reaches another panel. `via` names it; without one the endpoint's
|
|
96
|
+
* least invasive mechanism is used, which is the order the shared list keeps.
|
|
97
|
+
*/
|
|
98
|
+
private mechanismFor;
|
|
99
|
+
/** Whether a token this plugin already holds for a panel still works. */
|
|
100
|
+
private foreignTokenWorks;
|
|
101
|
+
/**
|
|
102
|
+
* A client for another panel: the plugin's own credential for that state root,
|
|
103
|
+
* minted and enrolled the first time it is needed, proved against the panel,
|
|
104
|
+
* and replaced when the panel no longer accepts it.
|
|
105
|
+
*
|
|
106
|
+
* home-hosted keeps only a hash, so an existing token this plugin does not hold
|
|
107
|
+
* can only be replaced — which is what choosing this mechanism means.
|
|
108
|
+
*/
|
|
109
|
+
private foreignClient;
|
|
110
|
+
/** Another panel's own config, refusing anything that is not a readable one. */
|
|
111
|
+
private readForeignConfig;
|
|
112
|
+
/** Another panel's entries as its config holds them: live status needs its API token. */
|
|
113
|
+
private listForeign;
|
|
114
|
+
private createForeign;
|
|
115
|
+
private updateForeign;
|
|
116
|
+
/** What a write actually left on disk, never what it was asked to write. */
|
|
117
|
+
private writtenForeignEntry;
|
|
118
|
+
private deleteForeign;
|
|
119
|
+
/**
|
|
120
|
+
* Start, stop or restart one entry through that panel's own CLI: this plugin
|
|
121
|
+
* holds no API token for a panel it does not manage, and a config edit cannot
|
|
122
|
+
* start anything. The CLI has no server restart, so a restart is a stop
|
|
123
|
+
* followed by a start — a stop that is not followed by one is reported.
|
|
124
|
+
*/
|
|
125
|
+
private lifecycleForeign;
|
|
63
126
|
private runtime;
|
|
64
127
|
/** The entry id this very process was started as, when the panel supervises us. */
|
|
65
128
|
selfEntryId(): string | null;
|
|
@@ -112,15 +175,6 @@ export declare class HomeHostedService extends Service {
|
|
|
112
175
|
private dshCommandRepair;
|
|
113
176
|
private writeOwned;
|
|
114
177
|
private panelRunning;
|
|
115
|
-
/**
|
|
116
|
-
* The version whose schema will parse what we write: the answering panel, or
|
|
117
|
-
* the CLI that will parse it next. `kill` did not exist before 0.6.0, and an
|
|
118
|
-
* older panel refuses to *boot* with it — so this is checked before any write.
|
|
119
|
-
*/
|
|
120
|
-
private configVersion;
|
|
121
|
-
/** Whether the panel whose schema parses our write knows `persistent`. */
|
|
122
|
-
private supportsPersistent;
|
|
123
|
-
private assertPolicySupported;
|
|
124
178
|
private applyIntents;
|
|
125
179
|
/**
|
|
126
180
|
* Stop managing an entry: restore what it was when we only adopted it, or
|
|
@@ -131,7 +185,7 @@ export declare class HomeHostedService extends Service {
|
|
|
131
185
|
/** Turn off the autostart intent for one entry, keeping the entry itself. */
|
|
132
186
|
private pauseManagedEntry;
|
|
133
187
|
/** Drive the panel's own UI through its CLI, and report what is installed. */
|
|
134
|
-
uiManage(action: UiAction, file?: string): Promise<UiResult>;
|
|
188
|
+
uiManage(action: UiAction, file?: string, target?: string): Promise<UiResult>;
|
|
135
189
|
private restoreEntry;
|
|
136
190
|
entriesStatus(): Promise<ManagedEntryStatus[]>;
|
|
137
191
|
private ladder;
|
|
@@ -147,6 +201,15 @@ export declare class HomeHostedService extends Service {
|
|
|
147
201
|
}>;
|
|
148
202
|
/** Out-of-the-box start: run the preferred CLI's `up`, which detaches itself. */
|
|
149
203
|
startPanelNow(): Promise<PanelControlResult>;
|
|
204
|
+
/**
|
|
205
|
+
* Stop the panel the plugin drives, and every server it supervises with it.
|
|
206
|
+
*
|
|
207
|
+
* Deliberately not a replacement for the takeover guard: stopping is safe to
|
|
208
|
+
* lose (an `autostart` entry comes back), while replacing is not. If this
|
|
209
|
+
* process is one of those servers the answer may never arrive, so the page
|
|
210
|
+
* treats a dropped call as "the panel is stopping" rather than a failure.
|
|
211
|
+
*/
|
|
212
|
+
stopPanelNow(): Promise<PanelControlResult>;
|
|
150
213
|
/**
|
|
151
214
|
* Replace an answering panel with the preferred copy.
|
|
152
215
|
*
|
|
@@ -179,7 +242,7 @@ export declare class HomeHostedService extends Service {
|
|
|
179
242
|
private ensureManagedEntry;
|
|
180
243
|
/** Re-assert the boot entry when the OS still has it but it stopped working. */
|
|
181
244
|
private repairBootEntry;
|
|
182
|
-
status(): Promise<HomeHostedStatus>;
|
|
245
|
+
status(refresh?: boolean): Promise<HomeHostedStatus>;
|
|
183
246
|
call(endpoint: RpcEndpoint, payload: unknown): Promise<unknown>;
|
|
184
247
|
}
|
|
185
248
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -21,7 +21,7 @@ export type Envelope<T> = {
|
|
|
21
21
|
error: RpcError;
|
|
22
22
|
};
|
|
23
23
|
/** Endpoints the browser and the agent tools both speak. */
|
|
24
|
-
export type RpcEndpoint = 'status' | 'servers.list' | 'servers.get' | 'servers.create' | 'servers.update' | 'servers.delete' | 'servers.start' | 'servers.stop' | 'servers.restart' | 'servers.freePort' | 'entries.apply' | 'entries.remove' | 'entries.restore' | 'boot.install' | 'boot.uninstall' | 'boot.verify' | 'panel.start' | 'panel.takeover' | 'panel.reclaimToken' | 'cli.installGlobal' | 'ui.manage' | 'settings.update';
|
|
24
|
+
export type RpcEndpoint = 'status' | 'servers.list' | 'servers.get' | 'servers.create' | 'servers.update' | 'servers.delete' | 'servers.start' | 'servers.stop' | 'servers.restart' | 'servers.freePort' | 'entries.apply' | 'entries.remove' | 'entries.restore' | 'boot.install' | 'boot.uninstall' | 'boot.verify' | 'panel.start' | 'panel.stop' | 'panel.takeover' | 'panel.reclaimToken' | 'cli.installGlobal' | 'ui.manage' | 'settings.update';
|
|
25
25
|
/** A settings write sends only the changed subtree; the host merges group by group. */
|
|
26
26
|
export interface SettingsPatch {
|
|
27
27
|
autostart?: Partial<PluginSettings['autostart']>;
|
|
@@ -30,36 +30,69 @@ export interface SettingsPatch {
|
|
|
30
30
|
panel?: Partial<PluginSettings['panel']>;
|
|
31
31
|
authNotice?: boolean;
|
|
32
32
|
reclaimToken?: boolean;
|
|
33
|
+
instancesNotice?: boolean;
|
|
33
34
|
uiStyle?: UiStyle;
|
|
34
35
|
agentTools?: Partial<PluginSettings['agentTools']>;
|
|
35
36
|
cli?: Partial<PluginSettings['cli']>;
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A state root to act on instead of the panel this plugin manages. Only a panel
|
|
40
|
+
* discovery reports is accepted, and only on the endpoints {@link FOREIGN_MECHANISMS}
|
|
41
|
+
* lists.
|
|
42
|
+
*/
|
|
43
|
+
export interface ForeignTarget {
|
|
44
|
+
/** A discovered panel's `--home`; omitted means the panel this plugin manages. */
|
|
45
|
+
home?: string;
|
|
46
|
+
/**
|
|
47
|
+
* How to reach it; omitted means the endpoint's first mechanism. Only a value
|
|
48
|
+
* {@link FOREIGN_MECHANISMS} lists for that endpoint is accepted.
|
|
49
|
+
*/
|
|
50
|
+
via?: ForeignMechanism;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* How this plugin can act on a panel it does not manage.
|
|
54
|
+
*
|
|
55
|
+
* `file` edits that panel's `servers.config.json`, `cli` runs the home-hosted
|
|
56
|
+
* CLI against its state root, and `api` enrols a token for it and drives its own
|
|
57
|
+
* HTTP API — the only way to reach some operations, and the only one that gives
|
|
58
|
+
* live status.
|
|
59
|
+
*/
|
|
60
|
+
export type ForeignMechanism = 'file' | 'cli' | 'api';
|
|
61
|
+
/**
|
|
62
|
+
* Which ways each endpoint has to reach another panel, least invasive first (the
|
|
63
|
+
* first entry is the default when a caller names no mechanism). A config edit
|
|
64
|
+
* needs no credential, the CLI needs none either, and `api` mints one.
|
|
65
|
+
*
|
|
66
|
+
* Anything absent here is about this machine or about the managed panel itself
|
|
67
|
+
* and is never aimed elsewhere.
|
|
68
|
+
*/
|
|
69
|
+
export declare const FOREIGN_MECHANISMS: Partial<Record<RpcEndpoint, readonly ForeignMechanism[]>>;
|
|
37
70
|
export interface EndpointPayloads {
|
|
38
|
-
/** `refresh`
|
|
71
|
+
/** `refresh` bypasses the short panel-inventory cache; status is live either way. */
|
|
39
72
|
'status': {
|
|
40
73
|
refresh?: boolean;
|
|
41
74
|
};
|
|
42
|
-
'servers.list':
|
|
75
|
+
'servers.list': ForeignTarget;
|
|
43
76
|
'servers.get': {
|
|
44
77
|
id: string;
|
|
45
78
|
};
|
|
46
|
-
'servers.create': {
|
|
79
|
+
'servers.create': ForeignTarget & {
|
|
47
80
|
entry: ServerEntry;
|
|
48
81
|
};
|
|
49
|
-
'servers.update': {
|
|
82
|
+
'servers.update': ForeignTarget & {
|
|
50
83
|
id: string;
|
|
51
84
|
patch: ServerEntryPatch;
|
|
52
85
|
};
|
|
53
|
-
'servers.delete': {
|
|
86
|
+
'servers.delete': ForeignTarget & {
|
|
54
87
|
id: string;
|
|
55
88
|
};
|
|
56
|
-
'servers.start': {
|
|
89
|
+
'servers.start': ForeignTarget & {
|
|
57
90
|
id: string;
|
|
58
91
|
};
|
|
59
|
-
'servers.stop': {
|
|
92
|
+
'servers.stop': ForeignTarget & {
|
|
60
93
|
id: string;
|
|
61
94
|
};
|
|
62
|
-
'servers.restart': {
|
|
95
|
+
'servers.restart': ForeignTarget & {
|
|
63
96
|
id: string;
|
|
64
97
|
};
|
|
65
98
|
'servers.freePort': {
|
|
@@ -84,6 +117,11 @@ export interface EndpointPayloads {
|
|
|
84
117
|
'boot.verify': Record<string, never>;
|
|
85
118
|
/** Start the preferred CLI as a detached panel; refused while one already answers. */
|
|
86
119
|
'panel.start': Record<string, never>;
|
|
120
|
+
/**
|
|
121
|
+
* Stop the answering panel through its own CLI. The servers it supervises stop
|
|
122
|
+
* with it, which can include the dsh this plugin is running in.
|
|
123
|
+
*/
|
|
124
|
+
'panel.stop': Record<string, never>;
|
|
87
125
|
/** Stop the answering panel and start the preferred copy instead. */
|
|
88
126
|
'panel.takeover': {
|
|
89
127
|
force?: boolean;
|
|
@@ -96,7 +134,7 @@ export interface EndpointPayloads {
|
|
|
96
134
|
/** Install the pinned range as a global CLI, so the `global` preference can use it. */
|
|
97
135
|
'cli.installGlobal': Record<string, never>;
|
|
98
136
|
/** Drive the panel's own UI: status, update, revert, or switch to a local build. */
|
|
99
|
-
'ui.manage': {
|
|
137
|
+
'ui.manage': ForeignTarget & {
|
|
100
138
|
action: UiAction;
|
|
101
139
|
file?: string;
|
|
102
140
|
};
|
|
@@ -237,6 +275,35 @@ export interface CliStatus {
|
|
|
237
275
|
global?: CliCandidate | null;
|
|
238
276
|
detail: string;
|
|
239
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* `home-hosted` is not one panel per machine: a person can install it per
|
|
280
|
+
* project, or run several panels from one install with different `--home`
|
|
281
|
+
* roots. The plugin drives exactly one of them — the root it resolved.
|
|
282
|
+
*/
|
|
283
|
+
export type InstanceSource = 'managed' | 'env' | 'sibling' | 'configured';
|
|
284
|
+
/** One home-hosted panel: its state root, and the facts readable from it. */
|
|
285
|
+
export interface InstanceView {
|
|
286
|
+
/** The state root itself: what `--home` / `$HHOSTED_HOME` names. */
|
|
287
|
+
home: string;
|
|
288
|
+
/**
|
|
289
|
+
* The panel this plugin drives. Every tool call targets it unless the call
|
|
290
|
+
* names another panel, and the plugin never writes to another one.
|
|
291
|
+
*/
|
|
292
|
+
managed: boolean;
|
|
293
|
+
/** This dsh process runs as one of this panel's entries (`HHOSTED_SERVER_ID`). */
|
|
294
|
+
hosting: boolean;
|
|
295
|
+
url: string | null;
|
|
296
|
+
port: number | null;
|
|
297
|
+
pid: number | null;
|
|
298
|
+
version: string | null;
|
|
299
|
+
/** A `run.json` whose pid is still alive. */
|
|
300
|
+
running: boolean;
|
|
301
|
+
projectDir: string | null;
|
|
302
|
+
/** Entries in that panel's `servers.config.json`; `null` when it is unreadable. */
|
|
303
|
+
servers: number | null;
|
|
304
|
+
/** How discovery found this root. */
|
|
305
|
+
source: InstanceSource;
|
|
306
|
+
}
|
|
240
307
|
export type BootMechanism = 'systemd-user' | 'systemd-system' | 'xdg-autostart' | 'launchd-agent' | 'launchd-daemon' | 'windows-run' | 'windows-task' | 'container' | 'unsupported';
|
|
241
308
|
export type BootState = 'not-installed' | 'installed-disabled' | 'enabled-running' | 'enabled-failing' | 'unsupported';
|
|
242
309
|
export interface BootCandidate {
|
|
@@ -269,7 +336,7 @@ export declare const AGENT_TOOL_NAMES: readonly AgentToolName[];
|
|
|
269
336
|
*/
|
|
270
337
|
export declare const MUTATING_AGENT_TOOLS: readonly AgentToolName[];
|
|
271
338
|
/** The shape this release writes; a file without it was written by 0.1.x. */
|
|
272
|
-
export declare const SETTINGS_VERSION =
|
|
339
|
+
export declare const SETTINGS_VERSION = 3;
|
|
273
340
|
export type UiStyle = 'detailed' | 'compact';
|
|
274
341
|
export interface PluginSettings {
|
|
275
342
|
version: number;
|
|
@@ -299,6 +366,12 @@ export interface PluginSettings {
|
|
|
299
366
|
* cleared it, or minted their own) should not keep an agent tool from working.
|
|
300
367
|
*/
|
|
301
368
|
reclaimToken: boolean;
|
|
369
|
+
/**
|
|
370
|
+
* Tell the agent how many home-hosted panels exist on this machine, and ask
|
|
371
|
+
* the user which one to act on when there is more than one. On by default: a
|
|
372
|
+
* plugin that owns one panel must not look like the only one there is.
|
|
373
|
+
*/
|
|
374
|
+
instancesNotice: boolean;
|
|
302
375
|
/** `detailed` shows cards and open disclosures; `compact` folds both away. */
|
|
303
376
|
uiStyle: UiStyle;
|
|
304
377
|
cli: {
|
|
@@ -321,6 +394,14 @@ export interface HomeHostedStatus {
|
|
|
321
394
|
/** The entry id this plugin manages; the page must not assume "dsh". */
|
|
322
395
|
defaultEntryId: string;
|
|
323
396
|
panel: PanelStatus;
|
|
397
|
+
/** The panel root this plugin instance drives, so a second install is visible. */
|
|
398
|
+
panelRoot?: string;
|
|
399
|
+
/** How that root was chosen; `legacy` means an existing `~/.home-hosted` panel was adopted. */
|
|
400
|
+
panelRootSource?: 'env' | 'legacy' | 'instance';
|
|
401
|
+
/** The OS boot artifact name this state root owns. */
|
|
402
|
+
bootUnitName?: string;
|
|
403
|
+
/** Every home-hosted panel found on this machine, the managed one first. */
|
|
404
|
+
instances?: InstanceView[];
|
|
324
405
|
boot: BootStatus;
|
|
325
406
|
entries: ManagedEntryStatus[];
|
|
326
407
|
servers: ServerEntryView[];
|
|
@@ -3,6 +3,27 @@ export declare function expandHome(value: string): string;
|
|
|
3
3
|
export declare function dshHome(): string;
|
|
4
4
|
/** home-hosted state root: `$HHOSTED_HOME`, else `~/.home-hosted`. */
|
|
5
5
|
export declare function homeHostedHome(): string;
|
|
6
|
+
/**
|
|
7
|
+
* The panel root one plugin instance drives.
|
|
8
|
+
*
|
|
9
|
+
* Two dsh installs share `~/.dsh` far more often than they should, and a shared
|
|
10
|
+
* panel root means two plugins editing one `servers.config.json` and one boot
|
|
11
|
+
* entry. So each instance normally gets its own root beside its own state.
|
|
12
|
+
*
|
|
13
|
+
* An explicit `$HHOSTED_HOME` is an instruction and always wins. So is a panel
|
|
14
|
+
* already living at the machine-wide default: an upgrade must never point a
|
|
15
|
+
* running panel somewhere else, or it would look like every server vanished.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveHomeHostedHome(stateDir: string, env?: NodeJS.ProcessEnv, homeDir?: string): {
|
|
18
|
+
home: string;
|
|
19
|
+
source: 'env' | 'legacy' | 'instance';
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The boot artifact one state root owns: the default keeps the historical name,
|
|
23
|
+
* and every other instance gets its own, so two plugins cannot overwrite one
|
|
24
|
+
* unit or delete the other's entry.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bootUnitName(stateDir: string): string;
|
|
6
27
|
/** Where this plugin keeps its own durable state (settings, snapshots, token). */
|
|
7
28
|
export declare function pluginStateDir(override?: string): string;
|
|
8
29
|
/** home-hosted's own tokens file, written by the panel at startup. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-home-hosted",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DeepSeek Harness (dsh) plugin: start your dsh web server automatically at boot, and manage home-hosted's panel and servers from inside dsh.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -117,6 +117,6 @@
|
|
|
117
117
|
"vitest": "^2.1.8"
|
|
118
118
|
},
|
|
119
119
|
"dependencies": {
|
|
120
|
-
"home-hosted": "^0.6.
|
|
120
|
+
"home-hosted": "^0.6.7"
|
|
121
121
|
}
|
|
122
122
|
}
|