Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions .github/tests/update-coverage-security.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ const applyChanges = (fx, env = {}) =>
...env,
});

const llmCredentials = 'uses: kernel/security-workflows/.github/actions/llm-credentials@';

test('actions are pinned and checkouts do not persist credentials', () => {
for (const entry of steps.filter((entry) => entry.includes('uses: '))) {
assert.match(entry, /uses: [\w/-]+@[a-f0-9]{40}(?:\s|$)/);
assert.match(entry, /uses: [\w./-]+@[a-f0-9]{40}(?:\s|$)/);
if (entry.includes('uses: actions/checkout@')) {
assert.match(entry, /persist-credentials: false/);
assert.doesNotMatch(entry, /token:/);
Expand All @@ -125,20 +127,26 @@ test('credentialed steps only run in the production SDK repository', () => {
assert.match(workflow, /\n generate:\n needs: prefetch\n if: github\.repository == 'kernel\/hypeman-go'\n/);
});

test('the Pi job references no secrets other than the model API keys', () => {
test('the Pi job references no secrets', () => {
// Every secret a job references is loaded into the runner at job start, and
// Pi could read it from runner memory with passwordless sudo.
// Pi could read it from runner memory with passwordless sudo. Pi gets model
// access from federated credentials, not secrets.
const piJobs = Object.keys(jobTexts).filter((name) => /^\s*pi --print\b/m.test(jobTexts[name]));
assert.deepEqual(piJobs, ['generate']);
const referenced = (text) => [...text.matchAll(/\bsecrets\b(?:\.([\w-]+))?/g)].map((match) => match[1] ?? '<all>');
assert.deepEqual(referenced(workflow.slice(0, workflow.indexOf('\njobs:\n'))), []);
for (const name of piJobs) {
assert.deepEqual(
[...new Set(referenced(jobTexts[name]))].sort(),
['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'],
`${name} may only reference the model API keys`,
);
assert.doesNotMatch(jobTexts[name], /github\.token|create-github-app-token|^ permissions:/m);
assert.deepEqual(referenced(jobTexts[name]), [], `${name} may not reference secrets`);
assert.doesNotMatch(jobTexts[name], /github\.token|create-github-app-token/);
// The only permission beyond reading the repository is the OIDC token for model credentials.
assert.match(jobTexts[name], /\n permissions:\n contents: read\n(?: #.*\n)? id-token: write\n outputs:/);
// Model credentials exist only while Pi runs.
const pi = jobs[name].indexOf(step(name, 'Update CLI coverage'));
assert.equal(jobs[name][pi - 1], step(name, 'Model credentials'));
const stop = jobs[name][pi + 1];
assert.equal(stop, step(name, 'Stop model credentials'));
assert.match(stop, /if: always\(\)\n[\s\S]*stop: true/);
assert.ok(jobs[name][pi - 1].includes(llmCredentials) && stop.includes(llmCredentials));
}
assert.match(workflow, /^permissions:\n contents: read\n\n/m);
});
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
permissions:
contents: read
pull-requests: write
# semgrep exchanges the job's OIDC token for short-lived model credentials.
id-token: write

jobs:
scan:
Expand Down
23 changes: 21 additions & 2 deletions .github/workflows/update-cli-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ jobs:
if: github.repository == 'kernel/hypeman-go'
runs-on: ubuntu-latest
timeout-minutes: 55
permissions:
contents: read
# The model credentials step exchanges the job's OIDC token for short-lived model credentials.
id-token: write
outputs:
changed: ${{ steps.snapshot.outputs.changed }}
main_sha: ${{ steps.cli-clone.outputs.main_sha }}
Expand Down Expand Up @@ -290,10 +294,19 @@ jobs:
echo "agent_base=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "agent_base_tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"

- name: Model credentials
uses: kernel/security-workflows/.github/actions/llm-credentials@4144994f1d6977e07ef5a5832599ee4faaae9550 # main
with:
model: ${{ vars.PI_PREFERRED_MODEL }}
anthropic-federation-rule-id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
anthropic-organization-id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
anthropic-service-account-id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
openai-identity-provider-id: ${{ vars.OPENAI_IDENTITY_PROVIDER_ID }}
openai-service-account-id: ${{ vars.OPENAI_SERVICE_ACCOUNT_ID }}
openai-audience: ${{ vars.OPENAI_WIF_AUDIENCE }}

- name: Update CLI coverage
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PI_PREFERRED_MODEL: ${{ vars.PI_PREFERRED_MODEL }}
PI_SKIP_VERSION_CHECK: '1'
PI_TELEMETRY: '0'
Expand Down Expand Up @@ -505,6 +518,12 @@ jobs:
- Ensure code compiles before finishing
"

- name: Stop model credentials
if: always()
uses: kernel/security-workflows/.github/actions/llm-credentials@4144994f1d6977e07ef5a5832599ee4faaae9550 # main
with:
stop: true

- name: Snapshot publishable changes
id: snapshot
working-directory: /tmp/hypeman-cli
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/vuln-remediation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
permissions:
contents: write
pull-requests: write
# vuln-remediation exchanges the job's OIDC token for short-lived model credentials.
id-token: write

jobs:
remediate:
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.28.0"
".": "0.29.0"
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.29.0](https://github.com/kernel/hypeman-go/compare/v0.28.0...v0.29.0) (2026-11-10)


### Features

* Add Windows hypervisor primitives ([e6c2b7b](https://github.com/kernel/hypeman-go/commit/e6c2b7bc0171807b2c891a26e9ab4f4bb786072b))
* Integrate vendor VFIO vGPUs into the instance lifecycle ([0872a65](https://github.com/kernel/hypeman-go/commit/0872a65a3733c6bfcc7449222cc4bd28bba43807))
* Place vGPU profiles the driver hides from creatable lists ([6b28176](https://github.com/kernel/hypeman-go/commit/6b281760d603033e2f78a87f5c24da8400b84585))
* Update Hypeman build toolchains to Go 1.26.10 ([4623fef](https://github.com/kernel/hypeman-go/commit/4623fef2ab5bf55dcac4a54ecbd841cc82368558))

## [0.28.0](https://github.com/kernel/hypeman-go/compare/v0.27.0...v0.28.0) (2026-09-01)


Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/kernel/hypeman-go@v0.28.0'
go get -u 'github.com/kernel/hypeman-go@v0.29.0'
```

<!-- x-release-please-end -->
Expand Down
8 changes: 4 additions & 4 deletions device.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ type Device struct {
// Whether the device is currently bound to the vfio-pci driver, which is required
// for VM passthrough.
//
// - true: Device is bound to vfio-pci and ready for (or currently in use by) a VM.
// The device's native driver has been unloaded.
// - false: Device is using its native driver (e.g., nvidia) or no driver. Hypeman
// will automatically bind to vfio-pci when attaching to an instance.
// - true: Device is bound to vfio-pci and ready for (or currently in use by) a VM.
// The device's native driver has been unloaded.
// - false: Device is using its native driver (e.g., nvidia) or no driver. Hypeman
// will automatically bind to vfio-pci when attaching to an instance.
BoundToVfio bool `json:"bound_to_vfio" api:"required"`
// Registration timestamp (RFC3339)
CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
Expand Down
12 changes: 6 additions & 6 deletions ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,9 @@ func (r *IngressRuleRequestHeaderAuthParam) UnmarshalJSON(data []byte) error {
type IngressTarget struct {
// Target instance name, ID, or capture reference.
//
// - For literal hostnames: Use the instance name or ID directly (e.g., "my-api")
// - For pattern hostnames: Reference a capture from the hostname (e.g.,
// "{instance}")
// - For literal hostnames: Use the instance name or ID directly (e.g., "my-api")
// - For pattern hostnames: Reference a capture from the hostname (e.g.,
// "{instance}")
//
// When using pattern hostnames, the instance is resolved dynamically at request
// time.
Expand Down Expand Up @@ -311,9 +311,9 @@ func (r IngressTarget) ToParam() IngressTargetParam {
type IngressTargetParam struct {
// Target instance name, ID, or capture reference.
//
// - For literal hostnames: Use the instance name or ID directly (e.g., "my-api")
// - For pattern hostnames: Reference a capture from the hostname (e.g.,
// "{instance}")
// - For literal hostnames: Use the instance name or ID directly (e.g., "my-api")
// - For pattern hostnames: Reference a capture from the hostname (e.g.,
// "{instance}")
//
// When using pattern hostnames, the instance is resolved dynamically at request
// time.
Expand Down
29 changes: 19 additions & 10 deletions instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,12 +788,15 @@ const (

// GPU information attached to the instance
type InstanceGPU struct {
// mdev device UUID
// sysfs path of the assigned vGPU device
DevicePath string `json:"device_path"`
// mdev device UUID (mdev hosts only)
MdevUuid string `json:"mdev_uuid"`
// vGPU profile name
Profile string `json:"profile"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
DevicePath respjson.Field
MdevUuid respjson.Field
Profile respjson.Field
ExtraFields map[string]respjson.Field
Expand Down Expand Up @@ -915,7 +918,8 @@ type InstanceStats struct {
MemoryRssBytes int64 `json:"memory_rss_bytes" api:"required"`
// Virtual Memory Size - total virtual memory allocated in bytes
MemoryVmsBytes int64 `json:"memory_vms_bytes" api:"required"`
// Total network bytes received by the VM (from TAP interface)
// Total network bytes received by the VM (from TAP interface). Always 0 for macOS
// guests, which use NAT with no per-instance interface.
NetworkRxBytes int64 `json:"network_rx_bytes" api:"required"`
// Total network bytes transmitted by the VM (from TAP interface)
NetworkTxBytes int64 `json:"network_tx_bytes" api:"required"`
Expand Down Expand Up @@ -1418,14 +1422,17 @@ type InstanceNewParams struct {
// Additional memory for hotplug (human-readable format like "3GB", "1G"). Omit to
// disable hotplug memory.
HotplugSize param.Opt[string] `json:"hotplug_size,omitzero"`
// Writable overlay disk size (human-readable format like "10GB", "50G")
// Writable overlay disk size (human-readable format like "10GB", "50G"). Omit this
// field for macOS guests, which clone the image's complete boot disk instead.
OverlaySize param.Opt[string] `json:"overlay_size,omitzero"`
// Target platform as os/arch[/variant] (e.g. "linux/amd64"), matching Docker
// --platform. Omit for the host platform. Not a fixed enum: the os/arch[/variant]
// grammar is validated server-side and invalid values return 400 invalid_platform.
// Only os "linux" with arch amd64 or arm64 is accepted today.
// Target platform as os/arch[/variant]. Linux container guests support amd64 and
// arm64. Experimental darwin/arm64 guests require an offline-imported macOS disk
// image and vz on Apple silicon. Omit to use the cached native image or host
// platform.
Platform param.Opt[string] `json:"platform,omitzero"`
// Base memory size (human-readable format like "1GB", "512MB", "2G")
// Base memory size (human-readable format like "1GB", "512MB", "2G"). macOS guests
// default to their image's memory; explicit sizes must be at least 4 GiB in whole
// MiB and within VZ host limits.
Size param.Opt[string] `json:"size,omitzero"`
// Skip guest-agent installation during boot. When true, the exec and stat APIs
// will not work for this instance. The instance will still run, but remote command
Expand All @@ -1440,7 +1447,8 @@ type InstanceNewParams struct {
// omit both expiration fields to disable automatic expiration. Mutually exclusive
// with expires_at.
Ttl param.Opt[string] `json:"ttl,omitzero"`
// Number of virtual CPUs
// Number of virtual CPUs. macOS guests default to their image's vCPUs; explicit
// counts must be at least 2 and within VZ host limits.
Vcpus param.Opt[int64] `json:"vcpus,omitzero"`
// Linux-only automatic standby policy based on active inbound TCP connections
// observed from the host conntrack table.
Expand Down Expand Up @@ -1790,7 +1798,7 @@ type InstanceLogsParams struct {
// - vmm: Cloud Hypervisor VMM logs (hypervisor stdout+stderr)
// - hypeman: Hypeman operations log (actions taken on this instance)
//
// Any of "app", "vmm", "hypeman".
// Any of "app", "vmm", "hypeman", "swtpm".
Source InstanceLogsParamsSource `query:"source,omitzero" json:"-"`
paramObj
}
Expand All @@ -1814,6 +1822,7 @@ const (
InstanceLogsParamsSourceApp InstanceLogsParamsSource = "app"
InstanceLogsParamsSourceVmm InstanceLogsParamsSource = "vmm"
InstanceLogsParamsSourceHypeman InstanceLogsParamsSource = "hypeman"
InstanceLogsParamsSourceSwtpm InstanceLogsParamsSource = "swtpm"
)

type InstanceStandbyParams struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

package internal

const PackageVersion = "0.28.0" // x-release-please-version
const PackageVersion = "0.29.0" // x-release-please-version
34 changes: 25 additions & 9 deletions resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ func (r *DiskBreakdown) UnmarshalJSON(data []byte) error {

// Available vGPU profile
type GPUProfile struct {
// Number of instances that can be created with this profile
// Number of additional instances of this profile the host can take if only this
// profile is added. Counts for different profiles are not additive: an instance of
// one profile can reduce availability of others on the same GPU.
Available int64 `json:"available" api:"required"`
// Frame buffer size in MB
FramebufferMB int64 `json:"framebuffer_mb" api:"required"`
Expand All @@ -107,27 +109,41 @@ func (r *GPUProfile) UnmarshalJSON(data []byte) error {

// GPU resource status. Null if no GPUs available.
type GPUResourceStatus struct {
// Free slots eligible for placement, matching admission control (excludes
// quarantined VFs; 0 while VF health state is unavailable)
AllocatableSlots int64 `json:"allocatable_slots" api:"required"`
// GPU mode (vgpu for SR-IOV/mdev, passthrough for whole GPU)
//
// Any of "vgpu", "passthrough".
Mode GPUResourceStatusMode `json:"mode" api:"required"`
// VFs quarantined after guest driver init failures (vGPU mode only). May overlap
// used_slots until the affected instance releases its VF.
QuarantinedSlots int64 `json:"quarantined_slots" api:"required"`
// Total slots (VFs for vGPU, physical GPUs for passthrough)
TotalSlots int64 `json:"total_slots" api:"required"`
// Slots currently in use
// Slots currently in use. Includes quarantined VFs that are still assigned, so
// this can overlap quarantined_slots.
UsedSlots int64 `json:"used_slots" api:"required"`
// Physical GPUs (only in passthrough mode)
Devices []PassthroughDevice `json:"devices"`
// Present when allocatable_slots is 0 because the VF health state could not be
// read or written rather than because the host is full. vGPU placement is refused
// until the state file is repaired or the next write succeeds.
PlacementDisabledReason string `json:"placement_disabled_reason"`
// Available vGPU profiles (only in vGPU mode)
Profiles []GPUProfile `json:"profiles"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
Mode respjson.Field
TotalSlots respjson.Field
UsedSlots respjson.Field
Devices respjson.Field
Profiles respjson.Field
ExtraFields map[string]respjson.Field
raw string
AllocatableSlots respjson.Field
Mode respjson.Field
QuarantinedSlots respjson.Field
TotalSlots respjson.Field
UsedSlots respjson.Field
Devices respjson.Field
PlacementDisabledReason respjson.Field
Profiles respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
}

Expand Down
Loading