@superjackfruitlabs/salaam 0.0.3 → 0.0.5
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/CHANGELOG.md +86 -0
- package/README.md +12 -11
- package/dist/src/acp-desk.js +140 -0
- package/dist/src/acp-outcome.js +182 -18
- package/dist/src/acp.js +296 -15
- package/dist/src/agent-profiles.js +45 -0
- package/dist/src/agentpod-authority.js +169 -11
- package/dist/src/agentpod-default-config.js +12 -0
- package/dist/src/agentpod-work-decision-protocol.js +40 -6
- package/dist/src/agentpod-work-decisions.js +108 -4
- package/dist/src/agentpod-work.js +1214 -127
- package/dist/src/approval-rules.js +287 -0
- package/dist/src/attach.js +3 -1
- package/dist/src/autonomy.js +80 -9
- package/dist/src/board-reads.js +106 -0
- package/dist/src/children.js +83 -0
- package/dist/src/cli-help.js +61 -6
- package/dist/src/cli.js +140 -4
- package/dist/src/command-danger.js +797 -0
- package/dist/src/command-network.js +200 -0
- package/dist/src/command-sandbox.js +327 -0
- package/dist/src/custom-commands.js +159 -0
- package/dist/src/danger-classifier.js +286 -0
- package/dist/src/execution-evidence.js +33 -1
- package/dist/src/git-helpers.js +160 -0
- package/dist/src/hooks.js +351 -0
- package/dist/src/host-bind.js +111 -0
- package/dist/src/host-breaker.js +88 -0
- package/dist/src/host-start.js +41 -1
- package/dist/src/host.js +284 -6
- package/dist/src/house-extension.js +292 -61
- package/dist/src/local-coding.js +726 -35
- package/dist/src/local-edit-helper.js +25 -0
- package/dist/src/local-edits.js +168 -6
- package/dist/src/local-undo.js +25 -16
- package/dist/src/managed.js +1 -1
- package/dist/src/mentions.js +277 -0
- package/dist/src/operator-mcp-cli.js +107 -0
- package/dist/src/operator-mcp.js +342 -0
- package/dist/src/persona.js +31 -0
- package/dist/src/pi-sessions.js +5 -1
- package/dist/src/pi.js +22 -1
- package/dist/src/project-instructions.js +195 -0
- package/dist/src/protocol.js +9 -3
- package/dist/src/rate-limit-retries.js +62 -0
- package/dist/src/recurring-work.js +1 -0
- package/dist/src/repository-snapshots.js +5 -2
- package/dist/src/run-grants.js +62 -0
- package/dist/src/service.js +482 -10
- package/dist/src/session-pick.js +77 -0
- package/dist/src/shell-passthrough.js +39 -0
- package/dist/src/station-checkpoints.js +139 -0
- package/dist/src/station-config.js +8 -1
- package/dist/src/station-files.js +20 -1
- package/dist/src/station-task-verification.js +6 -3
- package/dist/src/station-work.js +315 -22
- package/dist/src/store.js +70 -4
- package/dist/src/supervisor.js +408 -25
- package/dist/src/task-budget.js +39 -3
- package/dist/src/task-context.js +8 -2
- package/dist/src/task-critic.js +115 -0
- package/dist/src/task-driver.js +46 -2
- package/dist/src/task-policy.js +44 -12
- package/dist/src/task-verification.js +42 -9
- package/dist/src/task-work.js +47 -0
- package/dist/src/terminal-status.js +88 -0
- package/dist/src/tool-identity.js +69 -0
- package/dist/src/tui-activity.js +99 -0
- package/dist/src/tui-classifier.js +25 -0
- package/dist/src/tui-client.js +186 -5
- package/dist/src/tui-commands.js +23 -5
- package/dist/src/tui-composer.js +113 -0
- package/dist/src/tui-keymap.js +116 -0
- package/dist/src/tui-mentions.js +38 -0
- package/dist/src/tui-modals.js +157 -34
- package/dist/src/tui-notify.js +62 -0
- package/dist/src/tui-review.js +55 -0
- package/dist/src/tui-turn-keys.js +52 -0
- package/dist/src/tui-view.js +243 -69
- package/dist/src/tui.js +875 -118
- package/dist/src/turn-checkpoints.js +221 -0
- package/dist/src/turn-rewind.js +168 -0
- package/dist/src/usage.js +1 -1
- package/dist/src/web-fetch.js +114 -0
- package/dist/src/web-guard.js +163 -0
- package/dist/src/web-search.js +302 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/acp-desk.ts +155 -0
- package/src/acp-outcome.ts +174 -20
- package/src/acp.ts +386 -30
- package/src/agent-profiles.ts +48 -0
- package/src/agentpod-authority.ts +234 -15
- package/src/agentpod-default-config.ts +16 -0
- package/src/agentpod-work-decision-protocol.ts +69 -8
- package/src/agentpod-work-decisions.ts +123 -5
- package/src/agentpod-work.ts +1455 -152
- package/src/approval-rules.ts +360 -0
- package/src/attach.ts +4 -1
- package/src/autonomy.ts +111 -10
- package/src/board-reads.ts +148 -0
- package/src/children.ts +112 -0
- package/src/cli-help.ts +60 -6
- package/src/cli.ts +174 -4
- package/src/command-danger.ts +886 -0
- package/src/command-network.ts +215 -0
- package/src/command-sandbox.ts +393 -0
- package/src/custom-commands.ts +175 -0
- package/src/danger-classifier.ts +321 -0
- package/src/execution-evidence.ts +31 -1
- package/src/git-helpers.ts +177 -0
- package/src/hooks.ts +425 -0
- package/src/host-bind.ts +156 -0
- package/src/host-breaker.ts +89 -0
- package/src/host-start.ts +51 -1
- package/src/host.ts +365 -6
- package/src/house-extension.ts +313 -60
- package/src/local-coding.ts +894 -43
- package/src/local-edit-helper.ts +26 -0
- package/src/local-edits.ts +191 -8
- package/src/local-undo.ts +29 -14
- package/src/managed.ts +2 -2
- package/src/mentions.ts +304 -0
- package/src/operator-mcp-cli.ts +108 -0
- package/src/operator-mcp.ts +420 -0
- package/src/persona.ts +37 -0
- package/src/pi-sessions.ts +9 -1
- package/src/pi.ts +23 -1
- package/src/project-instructions.ts +229 -0
- package/src/protocol.ts +25 -3
- package/src/rate-limit-retries.ts +69 -0
- package/src/recurring-work.ts +1 -0
- package/src/repository-snapshots.ts +5 -2
- package/src/run-grants.ts +107 -0
- package/src/service.ts +709 -10
- package/src/session-pick.ts +102 -0
- package/src/shell-passthrough.ts +38 -0
- package/src/station-checkpoints.ts +183 -0
- package/src/station-config.ts +10 -1
- package/src/station-files.ts +20 -1
- package/src/station-task-verification.ts +6 -3
- package/src/station-work.ts +369 -20
- package/src/store.ts +70 -3
- package/src/supervisor.ts +463 -26
- package/src/task-budget.ts +48 -3
- package/src/task-context.ts +9 -2
- package/src/task-critic.ts +171 -0
- package/src/task-driver.ts +49 -2
- package/src/task-policy.ts +65 -13
- package/src/task-verification.ts +53 -9
- package/src/task-work.ts +75 -0
- package/src/terminal-status.ts +103 -0
- package/src/tool-identity.ts +110 -0
- package/src/tui-activity.ts +133 -0
- package/src/tui-classifier.ts +38 -0
- package/src/tui-client.ts +239 -8
- package/src/tui-commands.ts +49 -10
- package/src/tui-composer.ts +124 -0
- package/src/tui-keymap.ts +152 -0
- package/src/tui-mentions.ts +43 -0
- package/src/tui-modals.ts +173 -43
- package/src/tui-notify.ts +77 -0
- package/src/tui-review.ts +76 -0
- package/src/tui-turn-keys.ts +77 -0
- package/src/tui-view.ts +341 -88
- package/src/tui.ts +1047 -137
- package/src/turn-checkpoints.ts +253 -0
- package/src/turn-rewind.ts +258 -0
- package/src/usage.ts +1 -1
- package/src/web-fetch.ts +140 -0
- package/src/web-guard.ts +195 -0
- package/src/web-search.ts +402 -0
package/src/host-bind.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
closeSync,
|
|
4
|
+
fsyncSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
16
|
+
import { planHost } from './host.js';
|
|
17
|
+
import { canonical, digest } from './protocol.js';
|
|
18
|
+
import {
|
|
19
|
+
stationHash,
|
|
20
|
+
stationRuntimeFingerprint,
|
|
21
|
+
stationWorkspaceIdentity,
|
|
22
|
+
} from './station-config.js';
|
|
23
|
+
|
|
24
|
+
export interface BindInput {
|
|
25
|
+
organization_id: string;
|
|
26
|
+
node_id: string;
|
|
27
|
+
station_key: string;
|
|
28
|
+
workspace: string;
|
|
29
|
+
tools_user: string;
|
|
30
|
+
files: string;
|
|
31
|
+
runtime_root: string;
|
|
32
|
+
runtime_image_digest: string;
|
|
33
|
+
operator_group?: string;
|
|
34
|
+
dir?: string;
|
|
35
|
+
state?: string;
|
|
36
|
+
provider?: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
mcp_config?: string;
|
|
39
|
+
/** Item 9: the operator's persona file (root-owned, outside the workspace). */
|
|
40
|
+
persona?: string;
|
|
41
|
+
/** AgentPod-managed MCP profile: HTTP mcpServers the node assigns to admitted sessions. */
|
|
42
|
+
managed_mcp_config?: string;
|
|
43
|
+
/** Defaults to /usr/bin/systemd-run and /usr/bin/systemctl. */
|
|
44
|
+
systemd_run?: string;
|
|
45
|
+
systemctl?: string;
|
|
46
|
+
/** Tests only: where the default per-station directories live. */
|
|
47
|
+
roots?: { config: string; state: string };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const userName = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
51
|
+
function pin(path: string) {
|
|
52
|
+
const real = realpathSync(path);
|
|
53
|
+
return { path: real, sha256: stationHash(readFileSync(real)) };
|
|
54
|
+
}
|
|
55
|
+
/** Write a private file only when its content changes; report whether it did. */
|
|
56
|
+
function put(path: string, value: unknown): boolean {
|
|
57
|
+
const text = JSON.stringify(value, null, 2) + '\n';
|
|
58
|
+
if (existsSync(path) && readFileSync(path, 'utf8') === text) return false;
|
|
59
|
+
const tmp = path + '.tmp',
|
|
60
|
+
fd = openSync(tmp, 'w', 0o600);
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(fd, text);
|
|
63
|
+
fsyncSync(fd);
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
renameSync(tmp, path);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Bind one AgentPod station to a launcher-profile strict work host. Root on Linux.
|
|
72
|
+
* Every output is derived from the inputs; nothing is edited by hand. */
|
|
73
|
+
export async function bindHost(input: BindInput) {
|
|
74
|
+
if (process.platform !== 'linux' || process.getuid?.() !== 0)
|
|
75
|
+
throw Error('host bind runs as root on Linux');
|
|
76
|
+
const workspace = input.workspace;
|
|
77
|
+
if (!isAbsolute(workspace) || realpathSync(workspace) !== workspace)
|
|
78
|
+
throw Error('Use the canonical absolute workspace path');
|
|
79
|
+
const key = 'salaam:' + createHash('sha256').update(workspace).digest('hex').slice(0, 16);
|
|
80
|
+
if (input.station_key !== key)
|
|
81
|
+
throw Error('Station key does not name this workspace (expected ' + key + ')');
|
|
82
|
+
if (!userName.test(input.tools_user)) throw Error('tools user must be a user name');
|
|
83
|
+
try {
|
|
84
|
+
execFileSync('/usr/bin/getent', ['passwd', input.tools_user], { stdio: 'ignore' });
|
|
85
|
+
} catch {
|
|
86
|
+
throw Error('tools user does not exist');
|
|
87
|
+
}
|
|
88
|
+
if (input.tools_user === 'root') throw Error('tools user must not be root');
|
|
89
|
+
const id = key.slice('salaam:'.length),
|
|
90
|
+
roots = input.roots ?? { config: '/etc/salaam/stations', state: '/var/lib/salaam/stations' },
|
|
91
|
+
dir = resolve(input.dir ?? join(roots.config, id)),
|
|
92
|
+
state = resolve(input.state ?? join(roots.state, id, 'state'));
|
|
93
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
94
|
+
// Ancestors of state stay traversable so the operator group can reach operator.sock;
|
|
95
|
+
// state itself is the daemon's (0700, or 0710 for the operator group once served).
|
|
96
|
+
mkdirSync(join(state, '..'), { recursive: true, mode: 0o755 });
|
|
97
|
+
chmodSync(join(state, '..'), 0o755);
|
|
98
|
+
const pinsPath = join(dir, 'agentpod-pins.json'),
|
|
99
|
+
stationPath = join(dir, 'station-config.json'),
|
|
100
|
+
configPath = join(dir, 'host-config.json'),
|
|
101
|
+
manifestPath = join(dir, 'host.json');
|
|
102
|
+
let changed = put(pinsPath, {
|
|
103
|
+
version: 1,
|
|
104
|
+
organization_id: input.organization_id,
|
|
105
|
+
node_id: input.node_id,
|
|
106
|
+
station_key: key,
|
|
107
|
+
workspace,
|
|
108
|
+
});
|
|
109
|
+
const runtime = realpathSync(input.runtime_root);
|
|
110
|
+
changed =
|
|
111
|
+
put(stationPath, {
|
|
112
|
+
version: 1,
|
|
113
|
+
kind: 'linux-systemd-station-v1',
|
|
114
|
+
workspace,
|
|
115
|
+
workspace_identity: stationWorkspaceIdentity(workspace),
|
|
116
|
+
files: pin(input.files),
|
|
117
|
+
systemd_run: pin(input.systemd_run ?? '/usr/bin/systemd-run'),
|
|
118
|
+
systemctl: pin(input.systemctl ?? '/usr/bin/systemctl'),
|
|
119
|
+
runtime_root: runtime,
|
|
120
|
+
runtime_fingerprint: stationRuntimeFingerprint(runtime),
|
|
121
|
+
runtime_image_digest: input.runtime_image_digest,
|
|
122
|
+
tools_user: input.tools_user,
|
|
123
|
+
}) || changed;
|
|
124
|
+
const config = {
|
|
125
|
+
mode: 'agentpod',
|
|
126
|
+
state,
|
|
127
|
+
agentpod_config: pinsPath,
|
|
128
|
+
station_config: stationPath,
|
|
129
|
+
strict_work_version: 1,
|
|
130
|
+
...(input.operator_group ? { operator_group: input.operator_group } : {}),
|
|
131
|
+
...(input.provider ? { provider: input.provider } : {}),
|
|
132
|
+
...(input.model ? { model: input.model } : {}),
|
|
133
|
+
...(input.mcp_config ? { operator_mcp_config: realpathSync(input.mcp_config) } : {}),
|
|
134
|
+
...(input.persona ? { persona_config: realpathSync(input.persona) } : {}),
|
|
135
|
+
...(input.managed_mcp_config ? { mcp_config: realpathSync(input.managed_mcp_config) } : {}),
|
|
136
|
+
};
|
|
137
|
+
changed = put(configPath, config) || changed;
|
|
138
|
+
const manifest = await planHost(config);
|
|
139
|
+
const before = existsSync(manifestPath)
|
|
140
|
+
? canonical(JSON.parse(readFileSync(manifestPath, 'utf8')))
|
|
141
|
+
: null;
|
|
142
|
+
if (before !== canonical(manifest)) {
|
|
143
|
+
put(manifestPath, manifest);
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
version: 1,
|
|
148
|
+
station_key: key,
|
|
149
|
+
dir,
|
|
150
|
+
state,
|
|
151
|
+
manifest: manifestPath,
|
|
152
|
+
manifest_digest: digest(manifest),
|
|
153
|
+
unit: 'salaam-host@' + id + '.service',
|
|
154
|
+
changed,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A circuit breaker for agent fetches (operator MCP calls whose arguments carry a `url`). After
|
|
3
|
+
* repeated consecutive failures to one host within a session, calls to that host are paused for a
|
|
4
|
+
* short while: the agent gets a tool error saying so and can do something else, and the run's
|
|
5
|
+
* activity gets one line. Nothing fails the run, and a paused call is never made, so it records no
|
|
6
|
+
* operator MCP call. One success to the host closes the breaker again.
|
|
7
|
+
*/
|
|
8
|
+
export const BREAKER_FAILURES = 3;
|
|
9
|
+
export const BREAKER_PAUSE_MS = 60_000;
|
|
10
|
+
|
|
11
|
+
export function fetchHost(args: unknown): string | null {
|
|
12
|
+
const url = (args as { url?: unknown } | null)?.url;
|
|
13
|
+
if (typeof url !== 'string') return null;
|
|
14
|
+
try {
|
|
15
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
16
|
+
return host || null;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class HostBreaker {
|
|
23
|
+
private readonly hosts = new Map<string, { failures: number; until: number }>();
|
|
24
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
25
|
+
private key(session: string, host: string) {
|
|
26
|
+
return session + '\u0000' + host;
|
|
27
|
+
}
|
|
28
|
+
/** Milliseconds this host stays paused for the session, 0 when calls may go ahead. */
|
|
29
|
+
paused(session: string, host: string): number {
|
|
30
|
+
const h = this.hosts.get(this.key(session, host));
|
|
31
|
+
return h ? Math.max(0, h.until - this.now()) : 0;
|
|
32
|
+
}
|
|
33
|
+
/** Records a call's outcome; true when this failure just opened the breaker. */
|
|
34
|
+
record(session: string, host: string, ok: boolean): boolean {
|
|
35
|
+
const key = this.key(session, host);
|
|
36
|
+
if (ok) {
|
|
37
|
+
this.hosts.delete(key);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const h = this.hosts.get(key) ?? { failures: 0, until: 0 };
|
|
41
|
+
h.failures++;
|
|
42
|
+
this.hosts.set(key, h);
|
|
43
|
+
if (h.failures < BREAKER_FAILURES) return false;
|
|
44
|
+
h.failures = 0;
|
|
45
|
+
h.until = this.now() + BREAKER_PAUSE_MS;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
forget(session: string): void {
|
|
49
|
+
for (const key of this.hosts.keys())
|
|
50
|
+
if (key.startsWith(session + '\u0000')) this.hosts.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type McpResult = { content: { type: 'text'; text: string }[]; is_error: boolean };
|
|
55
|
+
/**
|
|
56
|
+
* One operator MCP call through the breaker. A paused host returns a tool error without calling;
|
|
57
|
+
* a thrown failure or a tool error counts against the host; the call that opens the breaker
|
|
58
|
+
* records `fetch.host_paused` for the run's activity. The call's own result or error is unchanged.
|
|
59
|
+
*/
|
|
60
|
+
export async function callThroughBreaker(
|
|
61
|
+
breaker: HostBreaker,
|
|
62
|
+
session: string,
|
|
63
|
+
args: unknown,
|
|
64
|
+
event: (payload: Record<string, unknown>) => void,
|
|
65
|
+
call: () => Promise<McpResult>,
|
|
66
|
+
): Promise<McpResult> {
|
|
67
|
+
const host = fetchHost(args);
|
|
68
|
+
if (!host) return call();
|
|
69
|
+
const wait = breaker.paused(session, host);
|
|
70
|
+
if (wait)
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: `Requests to ${host} are paused for ${Math.ceil(wait / 1000)}s after repeated failures; try another source or retry later.`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
is_error: true,
|
|
79
|
+
};
|
|
80
|
+
let ok = false;
|
|
81
|
+
try {
|
|
82
|
+
const result = await call();
|
|
83
|
+
ok = !result.is_error;
|
|
84
|
+
return result;
|
|
85
|
+
} finally {
|
|
86
|
+
if (breaker.record(session, host, ok))
|
|
87
|
+
event({ host, failures: BREAKER_FAILURES, pause_ms: BREAKER_PAUSE_MS });
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/host-start.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
2
|
import { constants, openSync, closeSync, fstatSync, mkdirSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { readHost, hostMcp, hostToolkit } from './host.js';
|
|
@@ -130,3 +130,53 @@ export async function startHost(path: string) {
|
|
|
130
130
|
release();
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
const launcherUnit = /^salaam-host@[a-f0-9]{16}\.service$/;
|
|
135
|
+
/** Launcher profile: an unprivileged operator asks systemd to start a root-owned,
|
|
136
|
+
* provisioned unit. It names only the unit; root reads its own reviewed manifest. */
|
|
137
|
+
export async function startHostUnit(
|
|
138
|
+
unit: string,
|
|
139
|
+
state: string,
|
|
140
|
+
options: { systemctl?: string; timeoutMs?: number } = {},
|
|
141
|
+
) {
|
|
142
|
+
if (!launcherUnit.test(unit)) throw Error('Unit must be salaam-host@<16 hex>.service');
|
|
143
|
+
if (realpathSync(state) !== state) throw Error('Use the canonical state path');
|
|
144
|
+
const socket = join(state, 'operator.sock'),
|
|
145
|
+
deadline = Date.now() + (options.timeoutMs ?? 20000);
|
|
146
|
+
const ready = async () => {
|
|
147
|
+
let h: Record<string, any>;
|
|
148
|
+
try {
|
|
149
|
+
h = await call(socket, '/v1/health', undefined, undefined, 2000);
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (!h.host_receipt || h.host_receipt.manifest_digest !== h.host_manifest_digest)
|
|
154
|
+
throw new ServiceError('host_mismatch', 'Running daemon reports no reviewed host receipt');
|
|
155
|
+
if (h.host_receipt.state !== state)
|
|
156
|
+
throw new ServiceError('host_mismatch', 'Running daemon serves another state directory');
|
|
157
|
+
if (h.host_supervisor_connected !== true || h.service !== 'healthy') return null;
|
|
158
|
+
if (
|
|
159
|
+
h.job_execution?.control_error ||
|
|
160
|
+
h.job_execution?.schedule_control_error ||
|
|
161
|
+
h.job_execution?.inbox_control_error
|
|
162
|
+
)
|
|
163
|
+
throw Error('Reviewed host controls are not ready');
|
|
164
|
+
return h.host_receipt as Record<string, unknown>;
|
|
165
|
+
};
|
|
166
|
+
let receipt = await ready();
|
|
167
|
+
if (receipt) return receipt;
|
|
168
|
+
await new Promise<void>((done, fail) =>
|
|
169
|
+
execFile(
|
|
170
|
+
options.systemctl ?? '/usr/bin/systemctl',
|
|
171
|
+
['start', '--no-ask-password', unit],
|
|
172
|
+
{ env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' }, timeout: 20000 },
|
|
173
|
+
(error) =>
|
|
174
|
+
error ? fail(Error('systemd refused to start ' + unit + '; inspect its journal')) : done(),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
while (!(receipt = await ready())) {
|
|
178
|
+
if (Date.now() >= deadline) throw Error('Host unit did not become ready; inspect its journal');
|
|
179
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
180
|
+
}
|
|
181
|
+
return receipt;
|
|
182
|
+
}
|