Commit a6063e5b authored by edy's avatar edy

fix(desktop): pass ingress ownership flags to workspace agent

parent d21ff2dd
...@@ -332,7 +332,9 @@ async function main(): Promise<void> { ...@@ -332,7 +332,9 @@ async function main(): Promise<void> {
sessionId: runtimeSession.sessionId, sessionId: runtimeSession.sessionId,
sessionKey: runtimeSession.sessionKey, sessionKey: runtimeSession.sessionKey,
workspaceDir: input.projectRoot, workspaceDir: input.projectRoot,
runId runId,
senderIsOwner: true,
allowModelOverride: true
}, silentRuntime); }, silentRuntime);
emit({ emit({
......
#!/usr/bin/env node
import { execFileSync, spawnSync } from "node:child_process"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { dirname, join, resolve } from "node:path"
const repoRoot = resolve(import.meta.dirname, "..", "..")
const tmpRoot = join(repoRoot, ".tmp", "workspace-agent-runner-smoke")
const compileDir = join(tmpRoot, "compiled")
let failures = 0
function check(label, condition, detail) {
if (condition) {
console.log(` PASS: ${label}`)
} else {
console.error(` FAIL: ${label}${detail ? ` - ${detail}` : ""}`)
failures++
}
}
function emitSummary() {
console.log("")
if (failures === 0) {
console.log("workspace-agent-runner-smoke: ALL PASSED")
} else {
console.error(`workspace-agent-runner-smoke: ${failures} FAILURE(S)`)
process.exit(1)
}
}
function writePackageFile(packageRoot, relativePath, content) {
const filePath = join(packageRoot, relativePath)
mkdirSync(dirname(filePath), { recursive: true })
writeFileSync(filePath, content.trimStart(), "utf8")
}
function preparePackage(caseRoot, files) {
const packageRoot = join(caseRoot, "openclaw-package")
mkdirSync(join(packageRoot, "dist"), { recursive: true })
mkdirSync(join(packageRoot, "node_modules"), { recursive: true })
writePackageFile(packageRoot, "package.json", JSON.stringify({ type: "module" }, null, 2))
for (const [relativePath, content] of Object.entries(files)) {
writePackageFile(packageRoot, relativePath, content)
}
return packageRoot
}
function parseEvents(stdout) {
return stdout
.split(/\r?\n/)
.filter((line) => line.startsWith("QJC_WORKSPACE_EVENT\t"))
.map((line) => JSON.parse(line.slice("QJC_WORKSPACE_EVENT\t".length)))
}
function defaultAttachments(projectRoot, name) {
return [{
id: `${name}-image`,
kind: "image",
name: "sample.png",
projectPath: join(projectRoot, "inputs", "images", "main", "sample.png"),
relativeProjectPath: "inputs/images/main/sample.png",
sizeBytes: 12,
mimeType: "image/png",
}]
}
function multiKindAttachments(projectRoot, name) {
return [
...defaultAttachments(projectRoot, name),
{
id: `${name}-pdf`,
kind: "pdf",
name: "brief.pdf",
projectPath: join(projectRoot, "inputs", "documents", "brief.pdf"),
relativeProjectPath: "inputs/documents/brief.pdf",
sizeBytes: 128,
mimeType: "application/pdf",
},
{
id: `${name}-xlsx`,
kind: "spreadsheet",
name: "metrics.xlsx",
projectPath: join(projectRoot, "inputs", "spreadsheets", "metrics.xlsx"),
relativeProjectPath: "inputs/spreadsheets/metrics.xlsx",
sizeBytes: 256,
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
{
id: `${name}-ppt`,
kind: "presentation",
name: "deck.pptx",
projectPath: join(projectRoot, "inputs", "presentations", "deck.pptx"),
relativeProjectPath: "inputs/presentations/deck.pptx",
sizeBytes: 512,
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
},
{
id: `${name}-mp3`,
kind: "audio",
name: "clip.mp3",
projectPath: join(projectRoot, "inputs", "audio", "clip.mp3"),
relativeProjectPath: "inputs/audio/clip.mp3",
sizeBytes: 1024,
mimeType: "audio/mpeg",
},
]
}
function runCase(compiledRunner, name, files, expectedContentPart, options = {}) {
const caseRoot = join(tmpRoot, name)
const projectRoot = join(caseRoot, "project")
const instrumentationDir = join(caseRoot, "instrumented")
rmSync(caseRoot, { recursive: true, force: true })
mkdirSync(projectRoot, { recursive: true })
const vendorPackageDir = preparePackage(caseRoot, files)
const attachments = options.attachments?.(projectRoot, name) || defaultAttachments(projectRoot, name)
const expectedAttachmentPaths = options.expectedAttachmentPaths || attachments.map((attachment) => attachment.relativeProjectPath)
const result = spawnSync(process.execPath, [compiledRunner], {
cwd: repoRoot,
encoding: "utf8",
input: JSON.stringify({
vendorPackageDir,
instrumentationDir,
projectRoot,
sessionId: `${name}-session`,
prompt: "Analyze this image",
attachments,
runId: `${name}-run`,
}),
})
const events = parseEvents(result.stdout)
const errorEvent = events.find((event) => event.type === "error")
const completedEvent = events.find((event) => event.type === "completed")
const deltaEvents = events.filter((event) => event.type === "delta")
check(`${name}: runner exited successfully`, result.status === 0, errorEvent?.message || result.stderr || `exit ${result.status}`)
check(`${name}: emitted delta`, deltaEvents.length > 0, JSON.stringify(events))
check(`${name}: completed`, Boolean(completedEvent), JSON.stringify(events))
check(`${name}: reply content`, completedEvent?.content?.includes(expectedContentPart), completedEvent?.content)
for (const relativePath of expectedAttachmentPaths) {
check(`${name}: attachment prelude includes ${relativePath}`, completedEvent?.content?.includes(relativePath), completedEvent?.content)
}
}
async function main() {
console.log("workspace-agent-runner-smoke: validating OpenClaw runner ingress options")
rmSync(tmpRoot, { recursive: true, force: true })
mkdirSync(compileDir, { recursive: true })
execFileSync("corepack", [
"pnpm",
"--dir",
join(repoRoot, "apps", "desktop"),
"exec",
"tsup",
"src/main/project-workspace-agent-runner.ts",
"--no-config",
"--format",
"cjs",
"--platform",
"node",
"--target",
"node20",
"--out-dir",
compileDir,
], {
cwd: repoRoot,
stdio: "inherit",
})
const compiledRunner = join(compileDir, "project-workspace-agent-runner.js")
check("compiled workspace agent runner", existsSync(compiledRunner), `not found: ${compiledRunner}`)
if (!existsSync(compiledRunner)) {
emitSummary()
return
}
const strictIngressOptionsSource = `
function assertIngressOptions(options) {
if (typeof options.senderIsOwner !== "boolean") {
throw new Error("senderIsOwner must be a boolean");
}
if (options.senderIsOwner !== true) {
throw new Error("senderIsOwner must be true");
}
if (typeof options.allowModelOverride !== "boolean") {
throw new Error("allowModelOverride must be a boolean");
}
if (options.allowModelOverride !== true) {
throw new Error("allowModelOverride must be true");
}
}
`
runCase(compiledRunner, "ingress-options", {
"dist/agent-legacy.js": `
import { emitAgentEvent, legacyModelMarker } from "./model-selection-legacy.js";
${strictIngressOptionsSource}
async function t(options) {
assertIngressOptions(options);
emitAgentEvent({ runId: options.runId, stream: "assistant", data: { delta: "leg" } });
return { payloads: [{ text: "legacy:" + legacyModelMarker() + ":" + options.message }] };
}
export { t };
`,
"dist/model-selection-legacy.js": `
const listeners = new Set();
function emitAgentEvent(event) {
for (const listener of listeners) {
listener(event);
}
}
function onAgentEvent(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function legacyModelMarker() {
return "direct-model-selection";
}
export { emitAgentEvent, legacyModelMarker, onAgentEvent };
`,
}, "legacy:direct-model-selection")
runCase(compiledRunner, "multi-attachment-prelude", {
"dist/agent-multi.js": `
import { emitAgentEvent, legacyModelMarker } from "./model-selection-multi.js";
${strictIngressOptionsSource}
async function t(options) {
assertIngressOptions(options);
emitAgentEvent({ runId: options.runId, stream: "assistant", data: { delta: "mul" } });
return { payloads: [{ text: "multi:" + legacyModelMarker() + ":" + options.message }] };
}
export { t };
`,
"dist/model-selection-multi.js": `
const listeners = new Set();
function emitAgentEvent(event) {
for (const listener of listeners) {
listener(event);
}
}
function onAgentEvent(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function legacyModelMarker() {
return "direct-model-selection";
}
export { emitAgentEvent, legacyModelMarker, onAgentEvent };
`,
}, "multi:direct-model-selection", {
attachments: multiKindAttachments,
expectedAttachmentPaths: [
"inputs/images/main/sample.png",
"inputs/documents/brief.pdf",
"inputs/spreadsheets/metrics.xlsx",
"inputs/presentations/deck.pptx",
"inputs/audio/clip.mp3",
],
})
rmSync(tmpRoot, { recursive: true, force: true })
emitSummary()
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error))
process.exit(1)
})
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"smoke:mac:workspace-entry": "node build/scripts/mac-workspace-entry-smoke.mjs", "smoke:mac:workspace-entry": "node build/scripts/mac-workspace-entry-smoke.mjs",
"smoke:mac:workspace-service": "node build/scripts/mac-workspace-service-smoke.mjs", "smoke:mac:workspace-service": "node build/scripts/mac-workspace-service-smoke.mjs",
"smoke:mac:workspace-startup": "node build/scripts/mac-workspace-startup-smoke.mjs", "smoke:mac:workspace-startup": "node build/scripts/mac-workspace-startup-smoke.mjs",
"smoke:workspace-agent-runner": "node build/scripts/workspace-agent-runner-smoke.mjs",
"smoke:bundled-runtime": "powershell -ExecutionPolicy Bypass -File build/scripts/bundled-runtime-smoke.ps1", "smoke:bundled-runtime": "powershell -ExecutionPolicy Bypass -File build/scripts/bundled-runtime-smoke.ps1",
"smoke:workspace-entry": "powershell -ExecutionPolicy Bypass -File build/scripts/workspace-entry-smoke.ps1", "smoke:workspace-entry": "powershell -ExecutionPolicy Bypass -File build/scripts/workspace-entry-smoke.ps1",
"smoke:cloud-bundle": "powershell -ExecutionPolicy Bypass -File build/scripts/cloud-bundle-smoke.ps1", "smoke:cloud-bundle": "powershell -ExecutionPolicy Bypass -File build/scripts/cloud-bundle-smoke.ps1",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment