feat(desktop): MSIX registers HermesGateway — desktop6:Service manifest extension
Task 2 of the gateway-as-MSIX-service plan (settled: user-context, 1903 floor, config-only demand-start): - before-build.mjs serviceExtensions(): the desktop6:Service fragment — Executable = the payload launcher hermes.exe (the same distlib PE serving the AppExecutionAliases; gateway run --service is the SCM frontend — NO shim binary), Name=HermesGateway, StartupType=demand (config-only posture: arrives stopped; `hermes gateway service on` flips to automatic-at-logon), StartAccount omitted (desktop6 default = installing user's context; localSystem explicitly rejected). light + store variants render service-less (store policy is the plan's open risk item). Rides the existing generated build/msix-extensions.xml via customExtensionsPath — same mechanism as the aliases + copilot-key fragments. - gen-msix-manifest.mjs: minVersion 10.0.17763 → 10.0.18362 (Win10 1903, the desktop6:Service requirement; settled support-matrix bump — 1809 is a 2018 OS; rides the release notes). - Verified: the REAL rendered manifest carries the service block (gen-msix-manifest bundled x64 → desktop6:Service Name=HermesGateway Executable=...hermes.exe); the staged fragment regenerates with it (caught + fixed a serviceFragment→ serviceExtensions name bug live via the real beforeBuild import). A flat-dir makeappx probe fails identically WITH and WITHOUT the fragment (the probe harness lacks signing identity) — the honest full validation is the win32 CI lane's real signed pack. - tests: 4 serviceExtensions contract tests (category/name/exe/ demand-start/one-block/xmlns-root/light-store-empty/name-prop) in cli-launchers.test.mjs — 15/15 pass; full scripts/ suite: my files green (1 pre-existing darwin-staging failure is the sibling agent's uncommitted territory).
This commit is contained in:
@@ -102,12 +102,53 @@ function writeMsixExtensions() {
|
||||
</uap3:Properties>
|
||||
</uap3:AppExtension>
|
||||
</uap3:Extension>
|
||||
${aliases}`
|
||||
${aliases}${serviceExtensions()}`
|
||||
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true })
|
||||
fs.writeFileSync(file, copilot)
|
||||
}
|
||||
|
||||
/**
|
||||
* The desktop6:Service fragment registering HermesGateway as a Windows
|
||||
* Service (plan: gateway-msix-windows-service; Task 2).
|
||||
*
|
||||
* The service Executable is the payload's own launcher (hermes.exe —
|
||||
* the distlib-minted PE that already serves the AppExecutionAliases):
|
||||
* its `gateway run --service` mode is the SCM frontend
|
||||
* (gateway/windows_service.py). NO separate shim binary.
|
||||
*
|
||||
* Settled decisions baked here (2026-09-03, ethie): user-context
|
||||
* service (StartAccount omitted — desktop6 defaults to the installing
|
||||
* user's context; localSystem explicitly rejected), demand StartupType
|
||||
* (config-only posture: the service arrives STOPPED; `hermes gateway
|
||||
* service on` flips it to automatic-at-logon).
|
||||
*
|
||||
* Content rules (the 0x80080204 playbook): xmlns:desktop6 rides the
|
||||
* fragment root; ONE extension block; Executable must name a real in-
|
||||
* package exe. Requires Win10 1903+ — the minVersion bump in
|
||||
* gen-msix-manifest.mjs matches. Exported pure for tests.
|
||||
*/
|
||||
export function serviceExtensions({
|
||||
name = 'HermesGateway',
|
||||
executable = ['app', 'resources', 'agent-payload', 'bin', 'hermes.exe'].join(String.fromCharCode(92)),
|
||||
variant = 'bundled',
|
||||
} = {}) {
|
||||
// light variant ships no payload → no service; the store variant is
|
||||
// decided by Store policy separately (plan risk item) — both render
|
||||
// service-less for now.
|
||||
if (variant === 'light' || variant === 'store') return ''
|
||||
return `<desktop6:Extension
|
||||
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
|
||||
Category="windows.service"
|
||||
Executable="${executable}"
|
||||
EntryPoint="Windows.FullTrustApplication">
|
||||
<desktop6:Service
|
||||
Name="${name}"
|
||||
StartupType="demand" />
|
||||
</desktop6:Extension>
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* One uap5:Extension block per payload CLI launcher, each naming its own
|
||||
* Executable (the distlib-minted launcher exes under bin/) and the alias
|
||||
|
||||
@@ -115,3 +115,46 @@ test('light variant emits no alias fragments', () => {
|
||||
assert.ok(appExecutionAliasExtensions(['hermes']).includes('windows.appExecutionAlias'))
|
||||
assert.ok(scriptDir.length > 0)
|
||||
})
|
||||
|
||||
// ── HermesGateway Windows Service fragment (Task 2, plan:
|
||||
// gateway-msix-windows-service) ────────────────────────────────────────
|
||||
import { serviceExtensions } from './before-build.mjs'
|
||||
|
||||
test('bundled variant registers HermesGateway, demand-start, launcher exe', () => {
|
||||
const frag = serviceExtensions()
|
||||
assert.ok(frag.includes('Category="windows.service"'), 'service category')
|
||||
assert.ok(frag.includes('Name="HermesGateway"'), 'the service name the CLI verbs key off')
|
||||
assert.ok(frag.includes('StartupType="demand"'), 'config-only posture: arrives stopped')
|
||||
// Compose the expected path the same way the source does — no escaping
|
||||
// ambiguity (the appExecutionAliasExtensions precedent).
|
||||
const bs = String.fromCharCode(92)
|
||||
const launcherPath = ['app', 'resources', 'agent-payload', 'bin', 'hermes.exe'].join(bs)
|
||||
assert.ok(
|
||||
frag.includes(`Executable="${launcherPath}"`),
|
||||
'the service Executable is the payload launcher (no shim binary)'
|
||||
)
|
||||
assert.ok(!frag.includes('StartAccount'), 'user-context by default — localSystem rejected')
|
||||
})
|
||||
|
||||
test('light and store variants render service-less', () => {
|
||||
assert.equal(serviceExtensions({ variant: 'light' }), '')
|
||||
assert.equal(serviceExtensions({ variant: 'store' }), '')
|
||||
})
|
||||
|
||||
test('one desktop6 extension block with xmlns on the fragment root', () => {
|
||||
const frag = serviceExtensions()
|
||||
assert.equal(
|
||||
(frag.match(/<desktop6:Extension/g) || []).length,
|
||||
1,
|
||||
'the 0x80080204 playbook: ONE extension block'
|
||||
)
|
||||
assert.ok(
|
||||
frag.includes('xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"'),
|
||||
'namespace rides the fragment root (the copilot-fragment precedent)'
|
||||
)
|
||||
})
|
||||
|
||||
test('custom name propagates (per-install namespacing)', () => {
|
||||
const frag = serviceExtensions({ name: 'HermesGateway_Tag1' })
|
||||
assert.ok(frag.includes('Name="HermesGateway_Tag1"'))
|
||||
})
|
||||
|
||||
@@ -105,8 +105,11 @@ const manifest = substituteManifestMacros(template, (m) => {
|
||||
case "resourceLanguages": return resourceLanguageTag(options.languages)
|
||||
case "capabilities": return `<Capabilities>\n${buildCapabilitiesXml(options.capabilities)}\n</Capabilities>`
|
||||
case "extensions": return extensions
|
||||
case "minVersion": return options.minVersion || "10.0.17763.0"
|
||||
case "maxVersionTested": return options.maxVersionTested || options.minVersion || "10.0.17763.0"
|
||||
// Win10 1903 (build 18362) floor: desktop6:Service — the MSIX-shipped
|
||||
// HermesGateway Windows Service — requires 1903+; settled 2026-09-03.
|
||||
// 1809 is a 2018 OS; the bump rides the release notes.
|
||||
case "minVersion": return options.minVersion || "10.0.18362.0"
|
||||
case "maxVersionTested": return options.maxVersionTested || options.minVersion || "10.0.18362.0"
|
||||
case "packageIntegrity": return ""
|
||||
default: throw new Error(`Macro ${m} is not defined`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user