external-subagent 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -7
- package/bin/external-subagent-mcp.mjs +3 -2
- package/cli/commands/agents.mjs +4 -9
- package/cli/commands/config.mjs +1 -1
- package/cli/commands/daemon.mjs +8 -3
- package/cli/commands/maintenance.mjs +158 -1
- package/cli/commands/plugin.mjs +28 -3
- package/cli/commands/tasks.mjs +7 -1
- package/cli/commands/update.mjs +18 -7
- package/cli/config/write.mjs +195 -14
- package/cli/constants.mjs +2 -2
- package/cli/install/codex.mjs +1 -1
- package/cli/install/dsh.mjs +606 -0
- package/cli/install/init.mjs +23 -33
- package/cli/install/layout.mjs +10 -2
- package/cli/install/payload.mjs +59 -12
- package/cli/install/service-activation.mjs +10 -2
- package/cli/install/service-linux.mjs +1 -1
- package/cli/install/service-macos.mjs +40 -14
- package/cli/install/service-windows.mjs +463 -0
- package/cli/install/update.mjs +6 -5
- package/cli/main.mjs +10 -14
- package/cli/maintenance.mjs +7 -1
- package/cli/paths.mjs +152 -8
- package/npm/native/README.md +2 -2
- package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
- package/npm/native/darwin-arm64/external-subagentd +0 -0
- package/npm/native/darwin-arm64/payload.json +5 -5
- package/npm/native/linux-x64/external-subagent-mcp +0 -0
- package/npm/native/linux-x64/external-subagentd +0 -0
- package/npm/native/linux-x64/payload.json +5 -5
- package/package.json +3 -2
- package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +5 -3
- package/schema/external-subagent-public-api.json +3 -3
- package/scripts/release/build-native-payload.mjs +22 -5
- package/scripts/release/check-native-tarball.mjs +34 -14
- package/scripts/release/stage-debug-plugin.mjs +9 -3
- package/scripts/release/test-installed-tarball.mjs +1 -1
- package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +0 -55
- package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +0 -19
- package/plugins/codex/external-subagent/hooks/guard-bash.mjs +0 -15
- package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +0 -206
- package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +0 -149
- package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +0 -58
package/cli/install/init.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
3
|
import { LAUNCH_AGENT_LABEL, NATIVE_DIR_NAME } from '../constants.mjs';
|
|
5
4
|
import { CliError } from '../errors.mjs';
|
|
6
5
|
import { platform } from '../paths.mjs';
|
|
@@ -13,7 +12,7 @@ import { updateInstallation } from './update.mjs';
|
|
|
13
12
|
import { loadInstallState, markInstallStep, removeCreatedDirectories, rollbackFiles, snapshotFile } from './recovery.mjs';
|
|
14
13
|
import { SYSTEMD_UNIT_NAME, bootstrapService, bootoutService, installServiceDefinition, runtimeObservations } from './service-macos.mjs';
|
|
15
14
|
|
|
16
|
-
// Standalone-install coordination (
|
|
15
|
+
// Standalone-install coordination (AUD-005 decision D1). A plain npm
|
|
17
16
|
// install only stages the package and payload; an explicit `init` installs
|
|
18
17
|
// the STANDALONE SERVICE ONLY — every state-changing step below is
|
|
19
18
|
// host-neutral:
|
|
@@ -47,12 +46,6 @@ import { SYSTEMD_UNIT_NAME, bootstrapService, bootoutService, installServiceDefi
|
|
|
47
46
|
// lets `init --resume` continue after environmental failures. Journals from
|
|
48
47
|
// pre-D1 inits may still list the retired codex steps; unknown completed ids
|
|
49
48
|
// are simply never consulted again.
|
|
50
|
-
const HOOK_INSTALLER = ['plugins', 'codex', 'external-subagent', 'scripts', 'install-agent-hooks.mjs'];
|
|
51
|
-
|
|
52
|
-
function hookInstallerPath() {
|
|
53
|
-
return path.join(packageRoot(), ...HOOK_INSTALLER);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
49
|
export function installPlan(paths, options = {}) {
|
|
57
50
|
// The plan reports the payload the verifier will read. When the running
|
|
58
51
|
// tuple has a supported payload this is the resolved manifest path; on an
|
|
@@ -70,22 +63,9 @@ export function installPlan(paths, options = {}) {
|
|
|
70
63
|
{ id: 'start-service', action: darwin ? 'bootstrap the daemon service' : 'enable and start the daemon service', path: paths.launchAgent, ...(darwin ? { label: LAUNCH_AGENT_LABEL } : { unit: SYSTEMD_UNIT_NAME }) },
|
|
71
64
|
{ id: 'publish-active-payload', action: 'publish the verified active payload and retained-byte baseline', path: paths.state },
|
|
72
65
|
];
|
|
73
|
-
if (options.installHooks) plan.splice(plan.findIndex((step) => step.id === 'publish-active-payload'), 0, { id: 'install-hooks', action: 'install ZCode policy hooks', path: paths.zcodeConfig, provenance: paths.hookProvenance });
|
|
74
66
|
return plan;
|
|
75
67
|
}
|
|
76
68
|
|
|
77
|
-
export function installHooks(paths, options = {}) {
|
|
78
|
-
if (options.dryRun) return { dry_run: true, plan: [{ id: 'install-hooks', action: 'install ZCode policy hooks', path: paths.zcodeConfig, provenance: paths.hookProvenance }] };
|
|
79
|
-
const result = spawnSync(process.execPath, [hookInstallerPath(), '--config', paths.zcodeConfig, '--provenance', paths.hookProvenance], { encoding: 'utf8' });
|
|
80
|
-
if (result.error) throw result.error;
|
|
81
|
-
if (result.status !== 0) {
|
|
82
|
-
let failure;
|
|
83
|
-
try { failure = JSON.parse((result.stderr || '').trim()); } catch { /* plain-text failure */ }
|
|
84
|
-
throw new CliError(failure?.code || 'HOOK_INSTALL_FAILED', failure?.error || (result.stderr || 'hook installation failed').trim());
|
|
85
|
-
}
|
|
86
|
-
try { return JSON.parse(result.stdout); } catch { throw new CliError('HOOK_INSTALL_FAILED', 'hook installer returned invalid JSON'); }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
69
|
export function runInit(options = {}) {
|
|
90
70
|
const paths = options.paths;
|
|
91
71
|
if (!paths) throw new CliError('INTERNAL_ERROR', 'runInit requires product paths');
|
|
@@ -104,12 +84,13 @@ export function runInit(options = {}) {
|
|
|
104
84
|
const payload = skipPayload ? { status: 'skipped', platform: null, version: null, files: [] } : verifyPayload();
|
|
105
85
|
const pathFindings = pathReport();
|
|
106
86
|
|
|
87
|
+
// Windows has no file-based service definition (paths.launchAgent is null),
|
|
88
|
+
// so there is nothing to snapshot or roll back there; the SCM registration
|
|
89
|
+
// is created by the start-service step instead.
|
|
107
90
|
const tracked = {
|
|
108
|
-
zcodeConfig: { file: paths.zcodeConfig, snapshot: snapshotFile(paths.zcodeConfig) },
|
|
109
|
-
hookProvenance: { file: paths.hookProvenance, snapshot: snapshotFile(paths.hookProvenance) },
|
|
110
91
|
state: { file: paths.state, snapshot: snapshotFile(paths.state) },
|
|
111
92
|
config: { file: paths.config, snapshot: snapshotFile(paths.config) },
|
|
112
|
-
launchAgent: { file: paths.launchAgent, snapshot: snapshotFile(paths.launchAgent) },
|
|
93
|
+
...(paths.launchAgent ? { launchAgent: { file: paths.launchAgent, snapshot: snapshotFile(paths.launchAgent) } } : {}),
|
|
113
94
|
};
|
|
114
95
|
const directories = {
|
|
115
96
|
data: { path: paths.data, existed: fs.existsSync(paths.data) },
|
|
@@ -130,6 +111,19 @@ export function runInit(options = {}) {
|
|
|
130
111
|
// A service that was already loaded before this init is never touched.
|
|
131
112
|
let serviceLoadedByThisRun = false;
|
|
132
113
|
let baseline = null;
|
|
114
|
+
// The service backends are selected by process platform. Windows has no
|
|
115
|
+
// process.getuid(), and every backend treats an undefined uid as "not
|
|
116
|
+
// applicable", so resolve it lazily instead of crashing before dispatch.
|
|
117
|
+
const currentUid = () => (typeof process.getuid === 'function' ? process.getuid() : undefined);
|
|
118
|
+
// Forward the injectable control seams (launchctl/systemctl/sc) and the
|
|
119
|
+
// explicit daemon artifact so a test or a controlled activation can own the
|
|
120
|
+
// service definition without touching the real service manager.
|
|
121
|
+
const serviceOptions = () => ({
|
|
122
|
+
launchctl: options.launchctl,
|
|
123
|
+
systemctl: options.systemctl,
|
|
124
|
+
sc: options.sc,
|
|
125
|
+
daemonPath: options.daemonPath,
|
|
126
|
+
});
|
|
133
127
|
try {
|
|
134
128
|
if (!completed.has('verify-payload')) mark('verify-payload');
|
|
135
129
|
if (!completed.has('check-path')) mark('check-path');
|
|
@@ -142,8 +136,8 @@ export function runInit(options = {}) {
|
|
|
142
136
|
// The daemon takes its database/socket/runtime from service arguments,
|
|
143
137
|
// never from this file; drop the retired top-level path fields so an
|
|
144
138
|
// install over an older local config republishes the current schema.
|
|
145
|
-
//
|
|
146
|
-
// before D1 rejected it, so
|
|
139
|
+
// The runtime_path remediation: `subagents.zcode.runtime_path` was a first-class config key
|
|
140
|
+
// before D1 rejected it, so an install predating that rejection can carry it in either
|
|
147
141
|
// the schema-2 or the legacy schema-1 shape. Retire it here — before
|
|
148
142
|
// parseConfig migrates/validates — so `npm new -> init` self-heals such
|
|
149
143
|
// configs instead of failing closed at the read gate (which stays
|
|
@@ -158,19 +152,15 @@ export function runInit(options = {}) {
|
|
|
158
152
|
mark('write-product-config');
|
|
159
153
|
}
|
|
160
154
|
if (!completed.has('install-launch-agent')) {
|
|
161
|
-
installServiceDefinition(paths);
|
|
155
|
+
installServiceDefinition(paths, serviceOptions());
|
|
162
156
|
failAt('install-launch-agent');
|
|
163
157
|
mark('install-launch-agent');
|
|
164
158
|
}
|
|
165
159
|
if (!completed.has('start-service') && !options.skipServiceStart) {
|
|
166
|
-
service = bootstrapService(paths,
|
|
160
|
+
service = bootstrapService(paths, currentUid(), serviceOptions());
|
|
167
161
|
serviceLoadedByThisRun = !service.skipped && !service.already_loaded;
|
|
168
162
|
mark('start-service');
|
|
169
163
|
}
|
|
170
|
-
if (options.installHooks && !completed.has('install-hooks')) {
|
|
171
|
-
installHooks(paths);
|
|
172
|
-
mark('install-hooks');
|
|
173
|
-
}
|
|
174
164
|
// The baseline publication runs LAST and only for an init that verified
|
|
175
165
|
// the payload it installed (a skipped probe — test harnesses only —
|
|
176
166
|
// leaves the resume journal as the sole state). It reuses the locked
|
|
@@ -202,7 +192,7 @@ export function runInit(options = {}) {
|
|
|
202
192
|
...removeCreatedDirectories(directories),
|
|
203
193
|
];
|
|
204
194
|
if (serviceLoadedByThisRun) {
|
|
205
|
-
try { bootoutService(paths,
|
|
195
|
+
try { bootoutService(paths, currentUid(), serviceOptions()); } catch (bootoutError) {
|
|
206
196
|
rollbackErrors.push(`service rollback (bootout) failed: ${bootoutError.message}`);
|
|
207
197
|
}
|
|
208
198
|
}
|
package/cli/install/layout.mjs
CHANGED
|
@@ -16,10 +16,18 @@ export const PLUGIN_NAME = PRODUCT_NAME;
|
|
|
16
16
|
// payload.mjs all key off the same value. A tuple outside this table has no
|
|
17
17
|
// payload and is rejected by the installer, never silently downgraded.
|
|
18
18
|
// NATIVE_PLATFORM stays the reference platform (the released macOS identity).
|
|
19
|
-
export const NATIVE_PLATFORMS = Object.freeze(['darwin-arm64', 'linux-x64']);
|
|
19
|
+
export const NATIVE_PLATFORMS = Object.freeze(['darwin-arm64', 'linux-x64', 'win32-x64']);
|
|
20
20
|
export const NATIVE_PLATFORM = NATIVE_PLATFORMS[0];
|
|
21
21
|
export const NATIVE_BINARIES = Object.freeze([DAEMON_BIN_NAME, MCP_BIN_NAME]);
|
|
22
22
|
|
|
23
|
+
// Payload artifacts are named with their host executable suffix. The staged
|
|
24
|
+
// Windows payload is the same product binaries with `.exe`; every consumer
|
|
25
|
+
// (verification, retention, service definition, MCP facade) resolves names
|
|
26
|
+
// through this one helper so a tuple never mixes stem and extension.
|
|
27
|
+
export function nativeExecutableName(name, platform = nativePlatform()) {
|
|
28
|
+
return typeof platform === 'string' && platform.startsWith('win32-') ? `${name}.exe` : name;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export function packageRoot() {
|
|
24
32
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
25
33
|
}
|
|
@@ -37,7 +45,7 @@ export function nativePayloadDir(platform = nativePlatform()) {
|
|
|
37
45
|
export function nativeBinary(name, platform = nativePlatform()) {
|
|
38
46
|
const dir = nativePayloadDir(platform);
|
|
39
47
|
if (!dir || !NATIVE_BINARIES.includes(name)) return null;
|
|
40
|
-
return path.join(dir, name);
|
|
48
|
+
return path.join(dir, nativeExecutableName(name, platform));
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export function payloadManifestPath(platform = nativePlatform()) {
|
package/cli/install/payload.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { CliError } from '../errors.mjs';
|
|
|
4
4
|
import { sha256 } from '../fs-atomic.mjs';
|
|
5
5
|
import { NATIVE_DIR_NAME, PRODUCT_NAME } from '../constants.mjs';
|
|
6
6
|
import {
|
|
7
|
-
NATIVE_PLATFORMS, cliVersion, nativePayloadDir, nativePlatform,
|
|
7
|
+
NATIVE_PLATFORMS, cliVersion, nativePayloadDir, nativePlatform, packageRoot, packageVersion,
|
|
8
8
|
} from './layout.mjs';
|
|
9
9
|
|
|
10
10
|
const MH_MAGIC_64 = 0xfeedfacf;
|
|
@@ -24,12 +24,37 @@ const EM_X86_64 = 0x3e;
|
|
|
24
24
|
const ELF_E_MACHINE_OFFSET = 18;
|
|
25
25
|
const ELF_HEADER_MIN_BYTES = ELF_E_MACHINE_OFFSET + 2;
|
|
26
26
|
|
|
27
|
+
// PE/COFF identity for the Windows payload:
|
|
28
|
+
// DOS header "MZ"
|
|
29
|
+
// e_lfanew uint32 LE at offset 0x3c (offset of the PE signature)
|
|
30
|
+
// PE signature "PE\0\0"
|
|
31
|
+
// Machine uint16 LE at peOffset + 4 (0x8664 = IMAGE_FILE_MACHINE_AMD64)
|
|
32
|
+
// Optional magic uint16 LE at peOffset + 24 (0x20b = PE32+)
|
|
33
|
+
// The image is a supported win32-x64 payload only when all four agree; any
|
|
34
|
+
// truncated header or other machine/format returns null.
|
|
35
|
+
const DOS_MAGIC = 0x5a4d; // "MZ"
|
|
36
|
+
const PE_SIGNATURE = 0x00004550; // "PE\0\0" as a LE uint32
|
|
37
|
+
const IMAGE_FILE_MACHINE_AMD64 = 0x8664;
|
|
38
|
+
const PE32_PLUS_MAGIC = 0x20b;
|
|
39
|
+
const PE_SIGNATURE_OFFSET = 0x3c;
|
|
40
|
+
const PE_HEADER_MIN_BYTES = PE_SIGNATURE_OFFSET + 4 + 20 + 2;
|
|
41
|
+
|
|
27
42
|
export function machoArch(bytes) {
|
|
28
43
|
if (bytes.length < 8) return null;
|
|
29
44
|
if (bytes.readUInt32LE(0) !== MH_MAGIC_64) return null;
|
|
30
45
|
return bytes.readUInt32LE(4) === CPU_TYPE_ARM64 ? 'arm64' : null;
|
|
31
46
|
}
|
|
32
47
|
|
|
48
|
+
export function peArch(bytes) {
|
|
49
|
+
if (bytes.length < PE_HEADER_MIN_BYTES) return null;
|
|
50
|
+
if (bytes.readUInt16LE(0) !== DOS_MAGIC) return null;
|
|
51
|
+
const peOffset = bytes.readUInt32LE(PE_SIGNATURE_OFFSET);
|
|
52
|
+
if (peOffset + 4 + 20 + 2 > bytes.length) return null;
|
|
53
|
+
if (bytes.readUInt32LE(peOffset) !== PE_SIGNATURE) return null;
|
|
54
|
+
if (bytes.readUInt16LE(peOffset + 4) !== IMAGE_FILE_MACHINE_AMD64) return null;
|
|
55
|
+
return bytes.readUInt16LE(peOffset + 24) === PE32_PLUS_MAGIC ? 'x64' : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
// The Linux image check mirrors machoArch: only a 64-bit little-endian x86-64
|
|
34
59
|
// ELF is a supported payload. A truncated header (shorter than e_machine), a
|
|
35
60
|
// 32-bit or big-endian image, or any other machine returns null and is
|
|
@@ -47,12 +72,24 @@ export function elfArch(bytes) {
|
|
|
47
72
|
const IMAGE_CHECKS = Object.freeze({
|
|
48
73
|
'darwin-arm64': { arch: 'arm64', image: 'Mach-O arm64 image', probe: machoArch },
|
|
49
74
|
'linux-x64': { arch: 'x64', image: 'ELF x86-64 image', probe: elfArch },
|
|
75
|
+
'win32-x64': { arch: 'x64', image: 'PE32+ x86-64 image', probe: peArch },
|
|
50
76
|
});
|
|
51
77
|
|
|
78
|
+
// Every released tuple carries a prebuilt payload, including win32-x64; the
|
|
79
|
+
// table is the verification identity set, not a distribution gate.
|
|
80
|
+
const PAYLOAD_PLATFORMS = Object.freeze([...NATIVE_PLATFORMS]);
|
|
81
|
+
|
|
82
|
+
function payloadDirectory(platform, root) {
|
|
83
|
+
if (root) return path.join(root, 'npm', NATIVE_DIR_NAME, platform);
|
|
84
|
+
return nativePayloadDir(platform)
|
|
85
|
+
?? (PAYLOAD_PLATFORMS.includes(platform) ? path.join(packageRoot(), 'npm', NATIVE_DIR_NAME, platform) : null);
|
|
86
|
+
}
|
|
87
|
+
|
|
52
88
|
export function readPayloadManifest(options = {}) {
|
|
53
89
|
const platform = options.platform || nativePlatform();
|
|
54
90
|
const root = options.root || undefined;
|
|
55
|
-
const
|
|
91
|
+
const dir = payloadDirectory(platform, root);
|
|
92
|
+
const file = dir === null ? null : path.join(dir, 'payload.json');
|
|
56
93
|
if (file === null || !fs.existsSync(file)) {
|
|
57
94
|
throw new CliError('PAYLOAD_MANIFEST_MISSING', `the ${platform || 'current'} platform payload manifest is missing from this package`);
|
|
58
95
|
}
|
|
@@ -73,9 +110,15 @@ export function readPayloadManifest(options = {}) {
|
|
|
73
110
|
// little-endian Mach-O arm64 image. A failed verification is an installation
|
|
74
111
|
// defect, never a silent downgrade path.
|
|
75
112
|
export function verifyPayload(options = {}) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
const explicitPlatform = options.platform !== undefined;
|
|
114
|
+
const platform = explicitPlatform ? options.platform : nativePlatform();
|
|
115
|
+
if (!PAYLOAD_PLATFORMS.includes(platform)) {
|
|
116
|
+
// The default resolver (nativePlatform) only produces the release tuples;
|
|
117
|
+
// win32-x64 is a controlled artifact reachable through an explicit option
|
|
118
|
+
// until the NATIVE_PLATFORMS gate opens. The message must not claim the
|
|
119
|
+
// rejected host tuple is available by default.
|
|
120
|
+
const supported = explicitPlatform ? PAYLOAD_PLATFORMS : NATIVE_PLATFORMS;
|
|
121
|
+
throw new CliError('UNSUPPORTED_PAYLOAD_PLATFORM', `no native payload exists for ${platform ?? `${process.platform}-${process.arch}`}; supported platforms: ${supported.join(', ')}`);
|
|
79
122
|
}
|
|
80
123
|
const image = IMAGE_CHECKS[platform];
|
|
81
124
|
const manifest = readPayloadManifest({ platform, root: options.root });
|
|
@@ -91,7 +134,8 @@ export function verifyPayload(options = {}) {
|
|
|
91
134
|
if (versions.package !== versions.cli || versions.payload !== versions.cli) {
|
|
92
135
|
throw new CliError('PAYLOAD_VERSION_MISMATCH', `payload ${versions.payload}, package ${versions.package}, and CLI ${versions.cli} versions disagree`);
|
|
93
136
|
}
|
|
94
|
-
const dir =
|
|
137
|
+
const dir = payloadDirectory(platform, options.root);
|
|
138
|
+
const posixMode = platform !== 'win32-x64' && process.platform !== 'win32';
|
|
95
139
|
const files = manifest.files.map((record) => {
|
|
96
140
|
if (!record || typeof record.name !== 'string' || path.basename(record.name) !== record.name || record.name.includes('..')) {
|
|
97
141
|
throw new CliError('PAYLOAD_MANIFEST_INVALID', 'payload file name must be a direct child');
|
|
@@ -101,7 +145,10 @@ export function verifyPayload(options = {}) {
|
|
|
101
145
|
try { stat = fs.statSync(target); } catch (error) {
|
|
102
146
|
throw new CliError('PAYLOAD_FILE_MISSING', `native payload file is missing: ${record.name} (${error.code})`);
|
|
103
147
|
}
|
|
104
|
-
|
|
148
|
+
// Windows has no POSIX execute bit: npm extraction cannot produce mode 755
|
|
149
|
+
// and the PE loader does not consult one, so the mode gate applies only to
|
|
150
|
+
// the POSIX payload identities (and only on a POSIX host).
|
|
151
|
+
if (!stat.isFile() || fs.lstatSync(target).isSymbolicLink() || (posixMode && (stat.mode & 0o777) !== 0o755)) {
|
|
105
152
|
throw new CliError('PAYLOAD_PERMISSIONS_INVALID', `native payload file ${record.name} must be a regular file with mode 755`);
|
|
106
153
|
}
|
|
107
154
|
const bytes = fs.readFileSync(target);
|
|
@@ -112,7 +159,7 @@ export function verifyPayload(options = {}) {
|
|
|
112
159
|
if (arch !== image.arch) {
|
|
113
160
|
throw new CliError('PAYLOAD_ARCH_UNSUPPORTED', `native payload file ${record.name} is not a ${image.image}`);
|
|
114
161
|
}
|
|
115
|
-
return { name: record.name, bytes: bytes.length, mode: '755', sha256: record.sha256, arch };
|
|
162
|
+
return { name: record.name, bytes: bytes.length, mode: posixMode ? '755' : null, sha256: record.sha256, arch };
|
|
116
163
|
});
|
|
117
164
|
return { status: 'verified', platform, version: manifest.version, files };
|
|
118
165
|
}
|
|
@@ -144,12 +191,12 @@ export function normalizePayloadMode(target) {
|
|
|
144
191
|
// mode so the install step can report what it changed.
|
|
145
192
|
export function normalizePayloadFiles(options = {}) {
|
|
146
193
|
const platform = options.platform || nativePlatform();
|
|
147
|
-
|
|
194
|
+
// Windows has no POSIX mode to repair; the extraction/permission repair is a
|
|
195
|
+
// POSIX-payload concern only.
|
|
196
|
+
if (!PAYLOAD_PLATFORMS.includes(platform) || platform === 'win32-x64') return [];
|
|
148
197
|
let manifest;
|
|
149
198
|
try { manifest = readPayloadManifest({ platform, root: options.root }); } catch { return []; }
|
|
150
|
-
const dir = options.root
|
|
151
|
-
? path.join(options.root, 'npm', NATIVE_DIR_NAME, platform)
|
|
152
|
-
: nativePayloadDir(platform);
|
|
199
|
+
const dir = payloadDirectory(platform, options.root);
|
|
153
200
|
const normalized = [];
|
|
154
201
|
for (const record of manifest.files) {
|
|
155
202
|
if (!record || typeof record.name !== 'string' || path.basename(record.name) !== record.name || record.name.includes('..')) continue;
|
|
@@ -7,6 +7,7 @@ import { LAUNCH_AGENT_LABEL } from '../constants.mjs';
|
|
|
7
7
|
import { platform } from '../paths.mjs';
|
|
8
8
|
import { launchAgentPlist, launchctl } from './service-macos.mjs';
|
|
9
9
|
import { SYSTEMD_UNIT_NAME, execStartProgram, replaceExecStartProgram, systemctl, systemdUnit, unitServiceStatus } from './service-linux.mjs';
|
|
10
|
+
import { activateServiceWindows } from './service-windows.mjs';
|
|
10
11
|
|
|
11
12
|
const xml = (s) => s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
12
13
|
const unxml = (s) => s.replaceAll('<', '<').replaceAll('>', '>').replaceAll('&', '&');
|
|
@@ -17,10 +18,17 @@ export function hasInstalledService(paths) {
|
|
|
17
18
|
return Boolean(paths.launchAgent && fs.existsSync(paths.launchAgent));
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// Platform dispatch
|
|
21
|
+
// Platform dispatch: the same activation health/rollback contract runs
|
|
21
22
|
// over launchd on macOS (verbatim) and the systemd user manager on Linux.
|
|
22
23
|
export async function activateService(paths, candidate, options = {}) {
|
|
23
|
-
|
|
24
|
+
const currentPlatform = platform();
|
|
25
|
+
if (currentPlatform === 'darwin') {
|
|
26
|
+
return activateLaunchdService(paths, candidate, options);
|
|
27
|
+
}
|
|
28
|
+
if (currentPlatform === 'win32') {
|
|
29
|
+
return activateServiceWindows(paths, candidate, options);
|
|
30
|
+
}
|
|
31
|
+
return activateSystemdService(paths, candidate, options);
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
// The service definition is derived from the CURRENT config: activation
|
|
@@ -23,7 +23,7 @@ import { readConfig } from '../config/read.mjs';
|
|
|
23
23
|
export const SYSTEMD_UNIT_NAME = `${PRODUCT_NAME}.service`;
|
|
24
24
|
export const SYSTEMCTL_PATH = '/usr/bin/systemctl';
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// A host without a reachable user manager gets an actionable
|
|
27
27
|
// explanation, never an automatic `loginctl enable-linger` and never a detour
|
|
28
28
|
// through the system manager.
|
|
29
29
|
const NO_SESSION_HINT = 'log in through a real session (which provides $XDG_RUNTIME_DIR), or run \'loginctl enable-linger <user>\' so the user manager stays available without one; this product never enables linger, never modifies unrelated units, and never contacts the system (PID 1) manager';
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
SYSTEMD_UNIT_NAME, bootstrapServiceSystemd, bootoutServiceSystemd, installServiceUnit,
|
|
13
13
|
systemdServiceRegistrationStatus,
|
|
14
14
|
} from './service-linux.mjs';
|
|
15
|
+
import {
|
|
16
|
+
bootstrapServiceWindows, bootoutServiceWindows, installServiceDefinitionWindows,
|
|
17
|
+
windowsServiceRegistrationStatus,
|
|
18
|
+
} from './service-windows.mjs';
|
|
15
19
|
|
|
16
20
|
// The macOS service backend for the product daemon, plus the platform
|
|
17
21
|
// dispatch into the systemd user backend (service-linux.mjs). The plist pins
|
|
@@ -169,36 +173,58 @@ export function bootoutLaunchdService(paths, uid = process.getuid(), options = {
|
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
|
|
172
|
-
// Platform dispatch
|
|
176
|
+
// Platform dispatch: the service backend is selected by the process
|
|
173
177
|
// platform — launchd on macOS, the systemd user manager on Linux — behind the
|
|
174
178
|
// same call surface every caller (init, start/stop, uninstall, status) already
|
|
175
179
|
// uses. The macOS arms below are the verbatim launchd implementations; the
|
|
176
180
|
// Linux arms carry the same idempotence/removal-confirmation contract through
|
|
177
181
|
// `systemctl --user` (options.systemctl is the injectable seam, the twin of
|
|
178
182
|
// options.launchctl).
|
|
179
|
-
export function serviceRegistrationStatus(uid
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
export function serviceRegistrationStatus(uid, options = {}) {
|
|
184
|
+
const currentPlatform = platform();
|
|
185
|
+
if (currentPlatform === 'darwin') {
|
|
186
|
+
return launchdServiceRegistrationStatus(uid ?? process.getuid(), options);
|
|
187
|
+
}
|
|
188
|
+
if (currentPlatform === 'win32') {
|
|
189
|
+
return windowsServiceRegistrationStatus(options);
|
|
190
|
+
}
|
|
191
|
+
return systemdServiceRegistrationStatus(options);
|
|
183
192
|
}
|
|
184
193
|
|
|
185
|
-
export function bootstrapService(paths, uid
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
export function bootstrapService(paths, uid, options = {}) {
|
|
195
|
+
const currentPlatform = platform();
|
|
196
|
+
if (currentPlatform === 'darwin') {
|
|
197
|
+
return bootstrapLaunchdService(paths, uid ?? process.getuid(), options);
|
|
198
|
+
}
|
|
199
|
+
if (currentPlatform === 'win32') {
|
|
200
|
+
return bootstrapServiceWindows(paths, options);
|
|
201
|
+
}
|
|
202
|
+
return bootstrapServiceSystemd(paths, uid, options);
|
|
189
203
|
}
|
|
190
204
|
|
|
191
|
-
export function bootoutService(paths, uid
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
export function bootoutService(paths, uid, options = {}) {
|
|
206
|
+
const currentPlatform = platform();
|
|
207
|
+
if (currentPlatform === 'darwin') {
|
|
208
|
+
return bootoutLaunchdService(paths, uid ?? process.getuid(), options);
|
|
209
|
+
}
|
|
210
|
+
if (currentPlatform === 'win32') {
|
|
211
|
+
return bootoutServiceWindows(paths, options);
|
|
212
|
+
}
|
|
213
|
+
return bootoutServiceSystemd(paths, uid, options);
|
|
195
214
|
}
|
|
196
215
|
|
|
197
216
|
// The service-definition installer behind init's install-launch-agent step:
|
|
198
217
|
// the launchd plist on macOS, the systemd user unit (0600, absolute verified
|
|
199
218
|
// payload, Restart=always) on Linux, both at paths.launchAgent.
|
|
200
219
|
export function installServiceDefinition(paths, options = {}) {
|
|
201
|
-
|
|
220
|
+
const currentPlatform = platform();
|
|
221
|
+
if (currentPlatform === 'darwin') {
|
|
222
|
+
return installLaunchAgent(paths, options);
|
|
223
|
+
}
|
|
224
|
+
if (currentPlatform === 'win32') {
|
|
225
|
+
return installServiceDefinitionWindows(paths, options);
|
|
226
|
+
}
|
|
227
|
+
return installServiceUnit(paths, options);
|
|
202
228
|
}
|
|
203
229
|
|
|
204
230
|
export { SYSTEMD_UNIT_NAME };
|