Skip to content

Commit ad14525

Browse files
simongdaviesCopilot
andcommitted
chore(deps): Bump spin to 0.12.0 and migrate to spin::LazyLock
Merge origin/main and resolve the runtime Cargo.toml to keep spin at 0.12 alongside the rquickjs 0.12 bump from main. spin 0.12 deprecates `spin::Lazy` in favour of `spin::LazyLock`; clippy runs with `-D warnings` so update the static RUNTIME in validator.rs accordingly to unblock the Lint & Test CI job. Regenerate the guest Cargo.lock. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
2 parents 8c2ffb0 + 7c30102 commit ad14525

14 files changed

Lines changed: 522 additions & 56 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2+
3+
# Manual probe: confirm the Tart/Ubuntu KVM runner actually exposes nested
4+
# virtualization inside the guest VM.
5+
#
6+
# This mirrors the macOS virtualization probe in spirit: it checks the runner
7+
# identity, asserts the expected ARM64/Linux environment, and fails if /dev/kvm
8+
# or kvm-ok are not available.
9+
10+
name: Check KVM ARM64 Runner
11+
12+
on:
13+
workflow_dispatch:
14+
# Path-scoped so it only runs on PRs that touch this probe, not every PR.
15+
pull_request:
16+
paths:
17+
- .github/workflows/check-kvm-arm64.yml
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
check-kvm:
24+
name: Inspect KVM on Tart ARM64 Linux runner
25+
runs-on: [self-hosted, arm64, kvm, linux, ubuntu-24.04]
26+
27+
steps:
28+
- name: Report runner identity
29+
id: identity
30+
run: |
31+
os_name="$(uname -s)"
32+
os_release="$(uname -r)"
33+
arch="$(uname -m)"
34+
kernel="$(uname -srv)"
35+
cpu_brand="$(lscpu 2>/dev/null | awk -F: '/^Model name/ {print $2}' | xargs || true)"
36+
37+
echo "::group::Runner identity"
38+
echo "OS: ${os_name} ${os_release}"
39+
echo "Architecture: ${arch}"
40+
echo "Kernel: ${kernel}"
41+
echo "CPU brand: ${cpu_brand}"
42+
echo "::endgroup::"
43+
44+
{
45+
echo "os_name=${os_name}"
46+
echo "os_release=${os_release}"
47+
echo "arch=${arch}"
48+
echo "cpu_brand=${cpu_brand}"
49+
} >> "$GITHUB_OUTPUT"
50+
51+
- name: Assert ARM64 Linux guest
52+
run: |
53+
arch="$(uname -m)"
54+
if [ "$arch" != "aarch64" ] && [ "$arch" != "arm64" ]; then
55+
echo "::error::Expected arm64/aarch64 runner, got '$arch'"
56+
exit 1
57+
fi
58+
echo "Confirmed ARM64 Linux runner."
59+
60+
- name: Verify /dev/kvm is present and usable
61+
id: kvm_device
62+
run: |
63+
if [ ! -e /dev/kvm ]; then
64+
echo "::error::/dev/kvm is missing on this runner"
65+
exit 1
66+
fi
67+
68+
ls -l /dev/kvm
69+
stat -c 'mode=%a owner=%U group=%G' /dev/kvm
70+
71+
if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then
72+
echo "::error::/dev/kvm exists but is not readable/writable by this user"
73+
exit 1
74+
fi
75+
76+
echo "kvm_present=true" >> "$GITHUB_OUTPUT"
77+
78+
- name: Verify kvm-ok is available and reports KVM
79+
id: kvm_ok
80+
run: |
81+
if ! command -v kvm-ok >/dev/null 2>&1; then
82+
echo "::error::kvm-ok not found on PATH"
83+
exit 1
84+
fi
85+
86+
echo "Found kvm-ok: $(command -v kvm-ok)"
87+
set +e
88+
kvm-ok
89+
rc=$?
90+
set -e
91+
92+
case "$rc" in
93+
0)
94+
echo "kvm_ok=true" >> "$GITHUB_OUTPUT"
95+
echo "kvm_ok_status=ok" >> "$GITHUB_OUTPUT"
96+
echo "KVM acceleration is available on this runner."
97+
;;
98+
*)
99+
echo "kvm_ok=false" >> "$GITHUB_OUTPUT"
100+
echo "kvm_ok_status=failed-${rc}" >> "$GITHUB_OUTPUT"
101+
echo "::error::kvm-ok failed with exit code $rc"
102+
exit 1
103+
;;
104+
esac
105+
106+
- name: Summary
107+
if: always()
108+
run: |
109+
{
110+
echo "### Tart KVM runner check"
111+
echo ""
112+
echo "| Property | Value |"
113+
echo "| --- | --- |"
114+
echo "| Runner label set | self-hosted, arm64, kvm, linux, ubuntu-24.04 |"
115+
echo "| OS | ${{ steps.identity.outputs.os_name }} ${{ steps.identity.outputs.os_release }} |"
116+
echo "| Architecture | ${{ steps.identity.outputs.arch }} |"
117+
echo "| CPU | ${{ steps.identity.outputs.cpu_brand || 'unknown' }} |"
118+
echo "| /dev/kvm present | ${{ steps.kvm_device.outputs.kvm_present || 'false' }} |"
119+
echo "| kvm-ok verdict | ${{ steps.kvm_ok.outputs.kvm_ok || 'not-run' }} (${{ steps.kvm_ok.outputs.kvm_ok_status || 'n/a' }}) |"
120+
} >> "$GITHUB_STEP_SUMMARY"

builtin-modules/tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"noEmitOnError": true,
1414
"skipLibCheck": true,
1515
"typeRoots": ["src/types"],
16-
"baseUrl": ".",
1716
"paths": {
1817
"ha:doc-core": ["./src/doc-core.ts"],
1918
"ha:ooxml-core": ["./src/ooxml-core.ts"],

package-lock.json

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"pngjs": "^7.0.0",
5454
"prettier": "^3.8.1",
5555
"tsx": "^4.0.0",
56-
"typescript": "^5.8.0",
56+
"typescript": "^6.0.3",
5757
"vitest": "^4.0.18"
5858
},
5959
"overrides": {

plugins/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"noEmitOnError": true,
1414
"skipLibCheck": true,
1515
"esModuleInterop": true,
16+
"types": ["node"],
1617
"paths": {
1718
"../../src/plugin-system/schema-types.js": ["../plugin-schema-types.ts"]
1819
}

src/agent/index.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
import { COMPLETION_STRINGS, renderHelp, renderTopicHelp } from "./commands.js";
6565
import { buildSystemMessage } from "./system-message.js";
6666
import { Spinner } from "./spinner.js";
67+
import { moduleInfoParameters } from "./module-info-schema.js";
6768
import { makeAuditProgressCallback } from "./audit-progress.js";
6869
import { createAgentState, type AgentState } from "./state.js";
6970
import {
@@ -1012,6 +1013,8 @@ const mcpWriteSafetyGate: WriteSafetyGate = async (
10121013
? C.err("⚠️ MCP DESTRUCTIVE operation")
10131014
: C.warn("⚠️ MCP write operation");
10141015

1016+
spinner.stop();
1017+
10151018
console.log();
10161019
console.log(` ${label}: ${C.label(serverName)}.${C.label(toolName)}`);
10171020
if (argSummary) {
@@ -5350,31 +5353,7 @@ const moduleInfoTool = defineTool("module_info", {
53505353
" - signatures: true for full parameter details on ALL functions (useful for API discovery)",
53515354
" - compact: true for condensed cheat sheet (just function names + required params)",
53525355
].join("\n"),
5353-
parameters: {
5354-
type: "object",
5355-
properties: {
5356-
name: {
5357-
type: "string",
5358-
description: "Module name (e.g. 'str-bytes', 'pptx')",
5359-
},
5360-
functionName: {
5361-
type: ["string", "array"],
5362-
description:
5363-
"Optional: get info for specific function(s). Accepts single name, comma-separated list, or array (e.g. 'chartSlide' or 'chartSlide,heroSlide,table' or ['chartSlide', 'heroSlide'])",
5364-
},
5365-
signatures: {
5366-
type: "boolean",
5367-
description:
5368-
"Optional: return full parameter types and descriptions for ALL functions (better for API discovery)",
5369-
},
5370-
compact: {
5371-
type: "boolean",
5372-
description:
5373-
"Optional: return condensed one-liner per export (just names + required params, no descriptions)",
5374-
},
5375-
},
5376-
required: ["name"],
5377-
},
5356+
parameters: moduleInfoParameters,
53785357
handler: async ({
53795358
name,
53805359
functionName,

src/agent/mcp/plugin-adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export function createMCPPluginAdapter(
9898
// Write-safety gate: check tools that are not known or inferred
9999
// read-only. The guest VM is paused during this check, so it is
100100
// safe to prompt the user.
101-
if (gate && !isReadOnlyMCPTool(tool)) {
101+
if (gate && !isReadOnlyMCPTool(tool, toolArgs)) {
102102
const allowed = await gate(
103103
conn.name,
104104
tool.name,

src/agent/mcp/tool-utils.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,54 @@ export function selectMCPTools(
127127
};
128128
}
129129

130-
export function isReadOnlyMCPTool(tool: MCPToolSchema): boolean {
130+
export function isReadOnlyMCPTool(
131+
tool: MCPToolSchema,
132+
args?: Record<string, unknown>,
133+
): boolean {
131134
if (tool.annotations?.readOnlyHint === true) return true;
132135
if (tool.annotations?.destructiveHint === true) return false;
133136

134137
const name = normaliseToolName(tool.name);
135138
if (WRITE_TOOL_PREFIXES.some((prefix) => name.startsWith(prefix))) {
136139
return false;
137140
}
138-
return READ_ONLY_TOOL_PREFIXES.some((prefix) => name.startsWith(prefix));
141+
if (READ_ONLY_TOOL_PREFIXES.some((prefix) => name.startsWith(prefix))) {
142+
return true;
143+
}
144+
145+
const operationHint = inferReadOnlyFromArgs(args);
146+
if (operationHint !== null) return operationHint;
147+
148+
return false;
149+
}
150+
151+
function inferReadOnlyFromArgs(
152+
args: Record<string, unknown> | undefined,
153+
): boolean | null {
154+
if (!args || typeof args !== "object") return null;
155+
156+
const candidates = [
157+
args.operation,
158+
args.action,
159+
args.command,
160+
args.method,
161+
args.kind,
162+
args.type,
163+
];
164+
165+
for (const candidate of candidates) {
166+
if (typeof candidate !== "string") continue;
167+
168+
const value = normaliseToolName(candidate);
169+
if (WRITE_TOOL_PREFIXES.some((prefix) => value.startsWith(prefix))) {
170+
return false;
171+
}
172+
if (READ_ONLY_TOOL_PREFIXES.some((prefix) => value.startsWith(prefix))) {
173+
return true;
174+
}
175+
}
176+
177+
return null;
139178
}
140179

141180
export function getMCPToolSafety(tool: MCPToolSchema): MCPToolInfo["safety"] {

src/agent/module-info-schema.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Parameter schema for the `module_info` tool.
2+
//
3+
// Extracted into its own module so the exact JSON Schema object that ships to
4+
// the Copilot/CAPI backend can be imported and validated by tests without
5+
// booting the agent (src/agent/index.ts runs main() on import).
6+
//
7+
// IMPORTANT: `functionName` accepts either a single string or an array of
8+
// strings. This MUST be expressed with `anyOf` rather than
9+
// `type: ["string", "array"]`. The CAPI schema validator rejects a union
10+
// `type` that includes "array" unless an `items` schema is also present,
11+
// producing: 400 Invalid schema ... array schema missing items.
12+
13+
/**
14+
* JSON Schema for the `module_info` tool parameters.
15+
*
16+
* Kept as a plain JSON Schema object (not Zod) to mirror exactly what is sent
17+
* to the backend.
18+
*/
19+
export const moduleInfoParameters = {
20+
type: "object",
21+
properties: {
22+
name: {
23+
type: "string",
24+
description: "Module name (e.g. 'str-bytes', 'pptx')",
25+
},
26+
functionName: {
27+
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
28+
description:
29+
"Optional: get info for specific function(s). Accepts single name, comma-separated list, or array (e.g. 'chartSlide' or 'chartSlide,heroSlide,table' or ['chartSlide', 'heroSlide'])",
30+
},
31+
signatures: {
32+
type: "boolean",
33+
description:
34+
"Optional: return full parameter types and descriptions for ALL functions (better for API discovery)",
35+
},
36+
compact: {
37+
type: "boolean",
38+
description:
39+
"Optional: return condensed one-liner per export (just names + required params, no descriptions)",
40+
},
41+
},
42+
required: ["name"],
43+
} as const;

0 commit comments

Comments
 (0)