palmier 0.5.2 → 0.5.4
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 +9 -9
- package/dist/agents/agent-instructions.md +7 -7
- package/dist/agents/shared-prompt.js +1 -1
- package/dist/client-store.d.ts +12 -0
- package/dist/client-store.js +57 -0
- package/dist/commands/clients.d.ts +4 -0
- package/dist/commands/clients.js +27 -0
- package/dist/commands/info.js +5 -5
- package/dist/commands/init.js +1 -1
- package/dist/commands/pair.js +4 -4
- package/dist/commands/run.js +9 -5
- package/dist/commands/serve.js +1 -1
- package/dist/events.js +1 -1
- package/dist/index.js +13 -13
- package/dist/platform/windows.d.ts +1 -1
- package/dist/platform/windows.js +33 -29
- package/dist/rpc-handler.js +7 -4
- package/dist/transports/http-transport.js +7 -7
- package/dist/transports/nats-transport.js +4 -4
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +7 -7
- package/src/agents/shared-prompt.ts +1 -1
- package/src/client-store.ts +68 -0
- package/src/commands/clients.ts +29 -0
- package/src/commands/info.ts +5 -5
- package/src/commands/init.ts +1 -1
- package/src/commands/pair.ts +4 -4
- package/src/commands/run.ts +8 -5
- package/src/commands/serve.ts +1 -1
- package/src/events.ts +1 -1
- package/src/index.ts +13 -13
- package/src/platform/windows.ts +29 -29
- package/src/rpc-handler.ts +7 -4
- package/src/transports/http-transport.ts +7 -7
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +3 -3
- package/test/agent-instructions.test.ts +22 -5
- package/test/agent-output-parsing.test.ts +12 -0
- package/test/windows-xml.test.ts +2 -0
- package/dist/commands/sessions.d.ts +0 -4
- package/dist/commands/sessions.js +0 -27
- package/dist/session-store.d.ts +0 -12
- package/dist/session-store.js +0 -57
- package/src/commands/sessions.ts +0 -29
- package/src/session-store.ts +0 -68
|
@@ -2,19 +2,19 @@ You are an AI agent executing a task on behalf of the user via the Palmier platf
|
|
|
2
2
|
|
|
3
3
|
## Reporting Output
|
|
4
4
|
|
|
5
|
-
If you generate report or output files, print each file path on its own line using this exact format
|
|
6
|
-
|
|
5
|
+
If you generate report or output files, print each file path on its own line using this exact format:
|
|
6
|
+
[PALMIER_REPORT] <filename>
|
|
7
7
|
|
|
8
8
|
## Completion
|
|
9
9
|
|
|
10
|
-
When you are done, output exactly one of these markers as the very last line (no
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
When you are done, output exactly one of these markers as the very last line (no other text on the same line):
|
|
11
|
+
[PALMIER_TASK_SUCCESS]
|
|
12
|
+
[PALMIER_TASK_FAILURE]
|
|
13
13
|
|
|
14
14
|
## Permissions
|
|
15
15
|
|
|
16
|
-
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format
|
|
17
|
-
|
|
16
|
+
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format:
|
|
17
|
+
[PALMIER_PERMISSION] <tool_name> | <description>
|
|
18
18
|
|
|
19
19
|
## HTTP Endpoints
|
|
20
20
|
|
|
@@ -14,7 +14,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
|
|
|
14
14
|
* Agent instructions with the serve daemon's HTTP port and task ID baked in.
|
|
15
15
|
*/
|
|
16
16
|
export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
|
|
17
|
-
const port = loadConfig().httpPort ??
|
|
17
|
+
const port = loadConfig().httpPort ?? 9966;
|
|
18
18
|
let instructions = AGENT_INSTRUCTIONS_TEMPLATE
|
|
19
19
|
.replace(/\{\{PORT\}\}/g, String(port))
|
|
20
20
|
.replace(/\{\{TASK_ID\}\}/g, taskId);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import { CONFIG_DIR } from "./config.js";
|
|
5
|
+
|
|
6
|
+
const CLIENTS_FILE = path.join(CONFIG_DIR, "clients.json");
|
|
7
|
+
|
|
8
|
+
export interface ClientEntry {
|
|
9
|
+
token: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readFile(): ClientEntry[] {
|
|
15
|
+
try {
|
|
16
|
+
if (!fs.existsSync(CLIENTS_FILE)) return [];
|
|
17
|
+
const raw = fs.readFileSync(CLIENTS_FILE, "utf-8");
|
|
18
|
+
return JSON.parse(raw) as ClientEntry[];
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeFile(clients: ClientEntry[]): void {
|
|
25
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
fs.writeFileSync(CLIENTS_FILE, JSON.stringify(clients, null, 2), "utf-8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadClients(): ClientEntry[] {
|
|
30
|
+
return readFile();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addClient(label?: string): ClientEntry {
|
|
34
|
+
const clients = readFile();
|
|
35
|
+
const entry: ClientEntry = {
|
|
36
|
+
token: randomBytes(32).toString("hex"),
|
|
37
|
+
createdAt: new Date().toISOString(),
|
|
38
|
+
...(label ? { label } : {}),
|
|
39
|
+
};
|
|
40
|
+
clients.push(entry);
|
|
41
|
+
writeFile(clients);
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function revokeClient(token: string): boolean {
|
|
46
|
+
const clients = readFile();
|
|
47
|
+
const idx = clients.findIndex((c) => c.token === token);
|
|
48
|
+
if (idx === -1) return false;
|
|
49
|
+
clients.splice(idx, 1);
|
|
50
|
+
writeFile(clients);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function revokeAllClients(): number {
|
|
55
|
+
const clients = readFile();
|
|
56
|
+
const count = clients.length;
|
|
57
|
+
writeFile([]);
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateClient(token: string): boolean {
|
|
62
|
+
const clients = readFile();
|
|
63
|
+
return clients.some((c) => c.token === token);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function hasClients(): boolean {
|
|
67
|
+
return readFile().length > 0;
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { loadClients, revokeClient, revokeAllClients } from "../client-store.js";
|
|
2
|
+
|
|
3
|
+
export async function clientsListCommand(): Promise<void> {
|
|
4
|
+
const clients = loadClients();
|
|
5
|
+
if (clients.length === 0) {
|
|
6
|
+
console.log("No active clients.");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
console.log(`${clients.length} active client(s):\n`);
|
|
11
|
+
for (const c of clients) {
|
|
12
|
+
const label = c.label ? ` (${c.label})` : "";
|
|
13
|
+
console.log(` ${c.token}${label} created ${c.createdAt}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function clientsRevokeCommand(token: string): Promise<void> {
|
|
18
|
+
if (revokeClient(token)) {
|
|
19
|
+
console.log("Client revoked.");
|
|
20
|
+
} else {
|
|
21
|
+
console.error("Client not found.");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function clientsRevokeAllCommand(): Promise<void> {
|
|
27
|
+
const count = revokeAllClients();
|
|
28
|
+
console.log(`Revoked ${count} client(s).`);
|
|
29
|
+
}
|
package/src/commands/info.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { loadClients } from "../client-store.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Print host connection info for setting up clients.
|
|
6
6
|
*/
|
|
7
7
|
export async function infoCommand(): Promise<void> {
|
|
8
8
|
const config = loadConfig();
|
|
9
|
-
const
|
|
9
|
+
const clients = loadClients();
|
|
10
10
|
|
|
11
11
|
console.log(`Host ID: ${config.hostId}`);
|
|
12
12
|
console.log(`Project root: ${config.projectRoot}`);
|
|
@@ -18,10 +18,10 @@ export async function infoCommand(): Promise<void> {
|
|
|
18
18
|
console.log(`Agents: (none detected — run \`palmier agents\`)`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
console.log(`
|
|
21
|
+
// Clients
|
|
22
|
+
console.log(`Clients: ${clients.length} active`);
|
|
23
23
|
|
|
24
|
-
if (
|
|
24
|
+
if (clients.length === 0) {
|
|
25
25
|
console.log("");
|
|
26
26
|
console.log("No paired clients. Run `palmier pair` to connect a device.");
|
|
27
27
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -44,7 +44,7 @@ export async function initCommand(): Promise<void> {
|
|
|
44
44
|
const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
|
|
45
45
|
const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
|
|
46
46
|
|
|
47
|
-
let httpPort =
|
|
47
|
+
let httpPort = 9966;
|
|
48
48
|
const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
|
|
49
49
|
const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
|
|
50
50
|
const parsed = parseInt(portAnswer.trim(), 10);
|
package/src/commands/pair.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as http from "node:http";
|
|
|
2
2
|
import { StringCodec } from "nats";
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
|
-
import {
|
|
5
|
+
import { addClient } from "../client-store.js";
|
|
6
6
|
import type { HostConfig } from "../types.js";
|
|
7
7
|
|
|
8
8
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
@@ -17,10 +17,10 @@ export function generatePairingCode(): string {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function buildPairResponse(config: HostConfig, label?: string) {
|
|
20
|
-
const
|
|
20
|
+
const client = addClient(label);
|
|
21
21
|
return {
|
|
22
22
|
hostId: config.hostId,
|
|
23
|
-
|
|
23
|
+
clientToken: client.token,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -67,7 +67,7 @@ function httpPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
67
67
|
export async function pairCommand(): Promise<void> {
|
|
68
68
|
const config = loadConfig();
|
|
69
69
|
const code = generatePairingCode();
|
|
70
|
-
const httpPort = config.httpPort ??
|
|
70
|
+
const httpPort = config.httpPort ?? 9966;
|
|
71
71
|
|
|
72
72
|
let paired = false;
|
|
73
73
|
|
package/src/commands/run.ts
CHANGED
|
@@ -70,7 +70,7 @@ async function invokeAgentWithRetries(
|
|
|
70
70
|
console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
|
|
71
71
|
const result = await spawnCommand(command, args, {
|
|
72
72
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
73
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ??
|
|
73
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
74
74
|
echoStdout: true,
|
|
75
75
|
resolveOnFailure: true,
|
|
76
76
|
stdin,
|
|
@@ -309,7 +309,7 @@ async function runCommandTriggeredMode(
|
|
|
309
309
|
|
|
310
310
|
const child = spawnStreamingCommand(commandStr, {
|
|
311
311
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
312
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ??
|
|
312
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
let linesProcessed = 0;
|
|
@@ -449,7 +449,7 @@ async function requestPermission(
|
|
|
449
449
|
taskDir: string,
|
|
450
450
|
requiredPermissions: RequiredPermission[],
|
|
451
451
|
): Promise<"granted" | "granted_all" | "aborted"> {
|
|
452
|
-
const port = config.httpPort ??
|
|
452
|
+
const port = config.httpPort ?? 9966;
|
|
453
453
|
const res = await fetch(`http://localhost:${port}/request-permission`, {
|
|
454
454
|
method: "POST",
|
|
455
455
|
headers: { "Content-Type": "application/json" },
|
|
@@ -477,7 +477,7 @@ async function requestConfirmation(
|
|
|
477
477
|
task: ParsedTask,
|
|
478
478
|
taskDir: string,
|
|
479
479
|
): Promise<boolean> {
|
|
480
|
-
const port = config.httpPort ??
|
|
480
|
+
const port = config.httpPort ?? 9966;
|
|
481
481
|
const res = await fetch(`http://localhost:${port}/request-confirmation`, {
|
|
482
482
|
method: "POST",
|
|
483
483
|
headers: { "Content-Type": "application/json" },
|
|
@@ -505,7 +505,8 @@ export function parseReportFiles(output: string): string[] {
|
|
|
505
505
|
let match;
|
|
506
506
|
while ((match = regex.exec(output)) !== null) {
|
|
507
507
|
const name = match[1].trim();
|
|
508
|
-
|
|
508
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<filename>")
|
|
509
|
+
if (name && !name.startsWith("<")) files.push(name);
|
|
509
510
|
}
|
|
510
511
|
return files;
|
|
511
512
|
}
|
|
@@ -520,6 +521,8 @@ export function parsePermissions(output: string): RequiredPermission[] {
|
|
|
520
521
|
let match;
|
|
521
522
|
while ((match = regex.exec(output)) !== null) {
|
|
522
523
|
const raw = match[1].trim();
|
|
524
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
|
|
525
|
+
if (raw.startsWith("<")) continue;
|
|
523
526
|
const sep = raw.indexOf("|");
|
|
524
527
|
if (sep !== -1) {
|
|
525
528
|
perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
|
package/src/commands/serve.ts
CHANGED
|
@@ -114,7 +114,7 @@ export async function serveCommand(): Promise<void> {
|
|
|
114
114
|
}, POLL_INTERVAL_MS);
|
|
115
115
|
|
|
116
116
|
const handleRpc = createRpcHandler(config, nc);
|
|
117
|
-
const httpPort = config.httpPort ??
|
|
117
|
+
const httpPort = config.httpPort ?? 9966;
|
|
118
118
|
|
|
119
119
|
// Start NATS transport (loops forever, fire-and-forget)
|
|
120
120
|
if (nc) {
|
package/src/events.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { serveCommand } from "./commands/serve.js";
|
|
|
12
12
|
|
|
13
13
|
import { pairCommand } from "./commands/pair.js";
|
|
14
14
|
import { restartCommand } from "./commands/restart.js";
|
|
15
|
-
import {
|
|
15
|
+
import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
@@ -68,29 +68,29 @@ program
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
const
|
|
72
|
-
.command("
|
|
73
|
-
.description("Manage paired
|
|
71
|
+
const clientsCmd = program
|
|
72
|
+
.command("clients")
|
|
73
|
+
.description("Manage paired clients");
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
clientsCmd
|
|
76
76
|
.command("list")
|
|
77
|
-
.description("List active
|
|
77
|
+
.description("List active clients")
|
|
78
78
|
.action(async () => {
|
|
79
|
-
await
|
|
79
|
+
await clientsListCommand();
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
clientsCmd
|
|
83
83
|
.command("revoke <token>")
|
|
84
|
-
.description("Revoke a
|
|
84
|
+
.description("Revoke a client by token")
|
|
85
85
|
.action(async (token: string) => {
|
|
86
|
-
await
|
|
86
|
+
await clientsRevokeCommand(token);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
clientsCmd
|
|
90
90
|
.command("revoke-all")
|
|
91
|
-
.description("Revoke all
|
|
91
|
+
.description("Revoke all clients")
|
|
92
92
|
.action(async () => {
|
|
93
|
-
await
|
|
93
|
+
await clientsRevokeAllCommand();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
// No subcommand → default to serve
|
package/src/platform/windows.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { getTaskDir, readTaskStatus } from "../task.js";
|
|
|
10
10
|
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
11
11
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
12
12
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
13
|
-
const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
@@ -68,6 +67,12 @@ export function buildTaskXml(tr: string, triggers: string[]): string {
|
|
|
68
67
|
return [
|
|
69
68
|
`<?xml version="1.0" encoding="UTF-16"?>`,
|
|
70
69
|
`<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
|
|
70
|
+
` <Principals>`,
|
|
71
|
+
` <Principal>`,
|
|
72
|
+
` <LogonType>S4U</LogonType>`,
|
|
73
|
+
` <RunLevel>LeastPrivilege</RunLevel>`,
|
|
74
|
+
` </Principal>`,
|
|
75
|
+
` </Principals>`,
|
|
71
76
|
` <Settings>`,
|
|
72
77
|
` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
|
|
73
78
|
` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
|
|
@@ -93,22 +98,16 @@ export class WindowsPlatform implements PlatformService {
|
|
|
93
98
|
installDaemon(config: HostConfig): void {
|
|
94
99
|
const script = process.argv[1] || "palmier";
|
|
95
100
|
|
|
96
|
-
// Create the Task Scheduler entry for the daemon
|
|
101
|
+
// Create the Task Scheduler entry for the daemon (BootTrigger starts it at system boot)
|
|
97
102
|
this.ensureDaemonTask(script);
|
|
98
103
|
|
|
99
|
-
// Registry Run key
|
|
100
|
-
// so the daemon always runs outside any session's job object.
|
|
101
|
-
const regValue = `schtasks /run /tn "\\Palmier\\${DAEMON_TASK_NAME}"`;
|
|
104
|
+
// Remove old Registry Run key if upgrading
|
|
102
105
|
try {
|
|
103
106
|
execFileSync("reg", [
|
|
104
|
-
"
|
|
105
|
-
"/v", DAEMON_TASK_NAME, "/
|
|
107
|
+
"delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
|
|
108
|
+
"/v", DAEMON_TASK_NAME, "/f",
|
|
106
109
|
], { encoding: "utf-8", stdio: "pipe" });
|
|
107
|
-
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.error(`Warning: failed to install registry run entry: ${err}`);
|
|
110
|
-
console.error("You may need to start palmier serve manually.");
|
|
111
|
-
}
|
|
110
|
+
} catch { /* key may not exist */ }
|
|
112
111
|
|
|
113
112
|
// Start the daemon now
|
|
114
113
|
this.startDaemonTask();
|
|
@@ -152,26 +151,30 @@ export class WindowsPlatform implements PlatformService {
|
|
|
152
151
|
this.startDaemonTask();
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
/** Create or update the Task Scheduler entry for the daemon. */
|
|
154
|
+
/** Create or update the Task Scheduler entry for the daemon (requires elevation for S4U). */
|
|
156
155
|
private ensureDaemonTask(script: string): void {
|
|
157
|
-
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
158
|
-
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
159
|
-
|
|
160
|
-
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
161
156
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
162
|
-
const tr = `"${
|
|
163
|
-
const xml = buildTaskXml(tr, [`<
|
|
157
|
+
const tr = `"${process.execPath}" "${script}" serve`;
|
|
158
|
+
const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`]);
|
|
164
159
|
const xmlPath = path.join(CONFIG_DIR, "daemon-task.xml");
|
|
165
160
|
try {
|
|
166
161
|
const bom = Buffer.from([0xFF, 0xFE]);
|
|
167
162
|
fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
|
|
168
|
-
|
|
163
|
+
// S4U LogonType requires elevation — spawn schtasks via RunAs
|
|
164
|
+
const args = `/create /tn "${tn}" /xml "${xmlPath}" /f`;
|
|
165
|
+
execFileSync("powershell", [
|
|
166
|
+
"-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '${args}'`,
|
|
167
|
+
], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
|
|
169
168
|
} catch (err: unknown) {
|
|
170
169
|
const e = err as { stderr?: string };
|
|
171
170
|
console.error(`Failed to create daemon task: ${e.stderr || err}`);
|
|
172
171
|
} finally {
|
|
173
172
|
try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
|
|
174
173
|
}
|
|
174
|
+
|
|
175
|
+
// Cleanup old VBS launcher if upgrading
|
|
176
|
+
const oldVbs = path.join(CONFIG_DIR, "daemon.vbs");
|
|
177
|
+
try { fs.unlinkSync(oldVbs); } catch { /* ignore */ }
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
/** Start the daemon via Task Scheduler (runs outside any session's job object). */
|
|
@@ -190,14 +193,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
190
193
|
const taskId = task.frontmatter.id;
|
|
191
194
|
const tn = schtasksTaskName(taskId);
|
|
192
195
|
const script = process.argv[1] || "palmier";
|
|
193
|
-
|
|
194
|
-
// Write a VBS launcher so the task runs without a visible console window
|
|
195
|
-
const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
|
|
196
|
-
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
|
|
197
|
-
fs.writeFileSync(vbsPath, vbs, "utf-8");
|
|
198
|
-
|
|
199
|
-
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
200
|
-
const tr = `"${wscript}" "${vbsPath}"`;
|
|
196
|
+
const tr = `"${process.execPath}" "${script}" run ${taskId}`;
|
|
201
197
|
|
|
202
198
|
// Build trigger XML elements
|
|
203
199
|
const triggerElements: string[] = [];
|
|
@@ -217,6 +213,8 @@ export class WindowsPlatform implements PlatformService {
|
|
|
217
213
|
|
|
218
214
|
// Write XML and register via schtasks — gives us full control over
|
|
219
215
|
// settings like MultipleInstancesPolicy that schtasks flags don't expose.
|
|
216
|
+
// S4U LogonType ensures no console window. Works without elevation
|
|
217
|
+
// because the daemon (which calls this) runs elevated.
|
|
220
218
|
const xml = buildTaskXml(tr, triggerElements);
|
|
221
219
|
const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
|
|
222
220
|
try {
|
|
@@ -232,6 +230,9 @@ export class WindowsPlatform implements PlatformService {
|
|
|
232
230
|
} finally {
|
|
233
231
|
try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
|
|
234
232
|
}
|
|
233
|
+
|
|
234
|
+
// Cleanup old VBS launcher if upgrading
|
|
235
|
+
try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
removeTaskTimer(taskId: string): void {
|
|
@@ -241,7 +242,6 @@ export class WindowsPlatform implements PlatformService {
|
|
|
241
242
|
} catch {
|
|
242
243
|
// Task might not exist — that's fine
|
|
243
244
|
}
|
|
244
|
-
try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
async startTask(taskId: string): Promise<void> {
|
package/src/rpc-handler.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { getPlatform } from "./platform/index.js";
|
|
|
11
11
|
import { spawnCommand } from "./spawn-command.js";
|
|
12
12
|
import crossSpawn from "cross-spawn";
|
|
13
13
|
import { getAgent } from "./agents/agent.js";
|
|
14
|
-
import {
|
|
14
|
+
import { validateClient } from "./client-store.js";
|
|
15
15
|
import { publishHostEvent } from "./events.js";
|
|
16
16
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
17
17
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
@@ -166,8 +166,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
async function handleRpc(request: RpcMessage): Promise<unknown> {
|
|
169
|
-
//
|
|
170
|
-
if (!request.localhost && (!request.
|
|
169
|
+
// Client token validation: skip for trusted localhost requests
|
|
170
|
+
if (!request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
171
171
|
return { error: "Unauthorized" };
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -259,7 +259,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
259
259
|
if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
260
260
|
if (params.requires_confirmation !== undefined)
|
|
261
261
|
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
262
|
-
if (params.yolo_mode !== undefined)
|
|
262
|
+
if (params.yolo_mode !== undefined) {
|
|
263
|
+
existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
|
|
264
|
+
if (params.yolo_mode) delete existing.frontmatter.permissions;
|
|
265
|
+
}
|
|
263
266
|
if (params.command !== undefined) {
|
|
264
267
|
if (params.command) {
|
|
265
268
|
existing.frontmatter.command = params.command;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import { StringCodec, type NatsConnection } from "nats";
|
|
4
|
-
import {
|
|
4
|
+
import { validateClient, addClient } from "../client-store.js";
|
|
5
5
|
import { registerPending } from "../pending-requests.js";
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
|
|
@@ -145,10 +145,10 @@ export async function startHttpTransport(
|
|
|
145
145
|
function checkAuth(req: http.IncomingMessage): boolean {
|
|
146
146
|
const auth = req.headers.authorization;
|
|
147
147
|
if (!auth || !auth.startsWith("Bearer ")) return false;
|
|
148
|
-
return
|
|
148
|
+
return validateClient(auth.slice(7));
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function
|
|
151
|
+
function extractClientToken(req: http.IncomingMessage): string | undefined {
|
|
152
152
|
const auth = req.headers.authorization;
|
|
153
153
|
if (!auth || !auth.startsWith("Bearer ")) return undefined;
|
|
154
154
|
return auth.slice(7);
|
|
@@ -394,11 +394,11 @@ export async function startHttpTransport(
|
|
|
394
394
|
const pending = pendingPairs.get(code);
|
|
395
395
|
if (!pending) { sendJson(res, 401, { error: "Invalid code" }); return; }
|
|
396
396
|
|
|
397
|
-
const
|
|
397
|
+
const client = addClient(label);
|
|
398
398
|
const ip = detectLanIp();
|
|
399
399
|
const response: Record<string, unknown> = {
|
|
400
400
|
hostId: config.hostId,
|
|
401
|
-
|
|
401
|
+
clientToken: client.token,
|
|
402
402
|
directUrl: `http://${ip}:${port}`,
|
|
403
403
|
};
|
|
404
404
|
|
|
@@ -476,11 +476,11 @@ export async function startHttpTransport(
|
|
|
476
476
|
}
|
|
477
477
|
} catch { sendJson(res, 400, { error: "Invalid JSON" }); return; }
|
|
478
478
|
|
|
479
|
-
const
|
|
479
|
+
const clientToken = extractClientToken(req);
|
|
480
480
|
console.log(`[http] RPC: ${method}`);
|
|
481
481
|
|
|
482
482
|
try {
|
|
483
|
-
const response = await handleRpc({ method, params,
|
|
483
|
+
const response = await handleRpc({ method, params, clientToken, localhost: isLocalhost(req) });
|
|
484
484
|
console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
|
|
485
485
|
sendJson(res, 200, response);
|
|
486
486
|
} catch (err) {
|
|
@@ -50,15 +50,15 @@ export async function startNatsTransport(
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Extract
|
|
54
|
-
const
|
|
55
|
-
delete params.
|
|
53
|
+
// Extract clientToken from params (PWA includes it in the payload)
|
|
54
|
+
const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
|
|
55
|
+
delete params.clientToken;
|
|
56
56
|
|
|
57
57
|
console.log(`[nats] RPC: ${method}`);
|
|
58
58
|
|
|
59
59
|
let response: unknown;
|
|
60
60
|
try {
|
|
61
|
-
response = await handleRpc({ method, params,
|
|
61
|
+
response = await handleRpc({ method, params, clientToken });
|
|
62
62
|
} catch (err) {
|
|
63
63
|
console.error(`[nats] RPC error (${method}):`, err);
|
|
64
64
|
response = { error: String(err) };
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface HostConfig {
|
|
|
9
9
|
// Detected agent CLIs
|
|
10
10
|
agents?: Array<{ key: string; label: string }>;
|
|
11
11
|
|
|
12
|
-
// HTTP server port (default
|
|
12
|
+
// HTTP server port (default 9966)
|
|
13
13
|
httpPort?: number;
|
|
14
14
|
// Whether to accept non-localhost HTTP connections
|
|
15
15
|
lanEnabled?: boolean;
|
|
@@ -79,7 +79,7 @@ export interface ConversationMessage {
|
|
|
79
79
|
export interface RpcMessage {
|
|
80
80
|
method: string;
|
|
81
81
|
params: Record<string, unknown>;
|
|
82
|
-
|
|
83
|
-
/** Trusted localhost request — skip
|
|
82
|
+
clientToken?: string;
|
|
83
|
+
/** Trusted localhost request — skip client validation. */
|
|
84
84
|
localhost?: boolean;
|
|
85
85
|
}
|
|
@@ -1,29 +1,46 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const templatePath = path.join(__dirname, "..", "src", "agents", "agent-instructions.md");
|
|
9
|
+
const template = fs.readFileSync(templatePath, "utf-8");
|
|
10
|
+
|
|
11
|
+
/** Minimal replica of getAgentInstructions that doesn't need host.json */
|
|
12
|
+
function buildInstructions(taskId: string, skipPermissions?: boolean): string {
|
|
13
|
+
let instructions = template
|
|
14
|
+
.replace(/\{\{PORT\}\}/g, "9966")
|
|
15
|
+
.replace(/\{\{TASK_ID\}\}/g, taskId);
|
|
16
|
+
if (skipPermissions) {
|
|
17
|
+
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
18
|
+
}
|
|
19
|
+
return instructions;
|
|
20
|
+
}
|
|
4
21
|
|
|
5
22
|
describe("getAgentInstructions", () => {
|
|
6
23
|
it("includes Permissions section by default", () => {
|
|
7
|
-
const result =
|
|
24
|
+
const result = buildInstructions("test-task-id");
|
|
8
25
|
assert.match(result, /## Permissions/);
|
|
9
26
|
assert.match(result, /PALMIER_PERMISSION/);
|
|
10
27
|
});
|
|
11
28
|
|
|
12
29
|
it("strips Permissions section when skipPermissions is true", () => {
|
|
13
|
-
const result =
|
|
30
|
+
const result = buildInstructions("test-task-id", true);
|
|
14
31
|
assert.doesNotMatch(result, /## Permissions/);
|
|
15
32
|
assert.doesNotMatch(result, /PALMIER_PERMISSION/);
|
|
16
33
|
});
|
|
17
34
|
|
|
18
35
|
it("preserves other sections when Permissions is stripped", () => {
|
|
19
|
-
const result =
|
|
36
|
+
const result = buildInstructions("test-task-id", true);
|
|
20
37
|
assert.match(result, /## Reporting Output/);
|
|
21
38
|
assert.match(result, /## Completion/);
|
|
22
39
|
assert.match(result, /## HTTP Endpoints/);
|
|
23
40
|
});
|
|
24
41
|
|
|
25
42
|
it("replaces template variables", () => {
|
|
26
|
-
const result =
|
|
43
|
+
const result = buildInstructions("my-task-123");
|
|
27
44
|
assert.match(result, /my-task-123/);
|
|
28
45
|
assert.doesNotMatch(result, /\{\{TASK_ID\}\}/);
|
|
29
46
|
assert.doesNotMatch(result, /\{\{PORT\}\}/);
|