palmier 0.5.2 → 0.5.3
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/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/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/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
package/README.md
CHANGED
|
@@ -46,9 +46,9 @@ All `palmier` commands should be run from a dedicated Palmier root directory (e.
|
|
|
46
46
|
|---|---|
|
|
47
47
|
| `palmier init` | Interactive setup wizard |
|
|
48
48
|
| `palmier pair` | Generate an OTP code to pair a new device |
|
|
49
|
-
| `palmier
|
|
50
|
-
| `palmier
|
|
51
|
-
| `palmier
|
|
49
|
+
| `palmier clients list` | List active client tokens |
|
|
50
|
+
| `palmier clients revoke <token>` | Revoke a specific client token |
|
|
51
|
+
| `palmier clients revoke-all` | Revoke all client tokens |
|
|
52
52
|
| `palmier info` | Show host connection info (address, mode) |
|
|
53
53
|
| `palmier serve` | Run the persistent RPC handler (default command) |
|
|
54
54
|
| `palmier restart` | Restart the palmier serve daemon |
|
|
@@ -70,17 +70,17 @@ Local access (`http://localhost:<port>`) works immediately — no pairing needed
|
|
|
70
70
|
|
|
71
71
|
For LAN or server mode, run `palmier pair` on the host to generate an OTP code. Enter it in the PWA — either at `http://<host-ip>:<port>` (LAN mode) or `https://app.palmier.me` (server mode).
|
|
72
72
|
|
|
73
|
-
### Managing
|
|
73
|
+
### Managing clients
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
# List all paired devices
|
|
77
|
-
palmier
|
|
77
|
+
palmier clients list
|
|
78
78
|
|
|
79
79
|
# Revoke a specific device's access
|
|
80
|
-
palmier
|
|
80
|
+
palmier clients revoke <token>
|
|
81
81
|
|
|
82
|
-
# Revoke all
|
|
83
|
-
palmier
|
|
82
|
+
# Revoke all clients (unpair all devices)
|
|
83
|
+
palmier clients revoke-all
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
The `init` command:
|
|
@@ -126,7 +126,7 @@ palmier restart
|
|
|
126
126
|
## How It Works
|
|
127
127
|
|
|
128
128
|
- The host runs as a **background daemon** (systemd user service on Linux, Registry Run key on Windows), staying alive via `palmier serve`.
|
|
129
|
-
- **Device access** — localhost is always trusted (no pairing needed). LAN and server mode devices communicate via direct HTTP or NATS respectively, and must pair via OTP to get a
|
|
129
|
+
- **Device access** — localhost is always trusted (no pairing needed). LAN and server mode devices communicate via direct HTTP or NATS respectively, and must pair via OTP to get a client token.
|
|
130
130
|
- **Tasks** are stored locally as Markdown files in a `tasks/` directory. Each task has a name, prompt, execution plan, and optional schedules (cron schedules or one-time dates).
|
|
131
131
|
- **Plan generation** is automatic — when you create or update a task, the host invokes your chosen agent CLI to generate an execution plan and name.
|
|
132
132
|
- **Schedules** are backed by systemd timers (Linux) or Task Scheduler (Windows). You can enable/disable them without deleting the task, and any task can still be run manually at any time.
|
|
@@ -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
|
|
|
@@ -8,7 +8,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(path.join(__dirname, "agent-
|
|
|
8
8
|
* Agent instructions with the serve daemon's HTTP port and task ID baked in.
|
|
9
9
|
*/
|
|
10
10
|
export function getAgentInstructions(taskId, skipPermissions) {
|
|
11
|
-
const port = loadConfig().httpPort ??
|
|
11
|
+
const port = loadConfig().httpPort ?? 9966;
|
|
12
12
|
let instructions = AGENT_INSTRUCTIONS_TEMPLATE
|
|
13
13
|
.replace(/\{\{PORT\}\}/g, String(port))
|
|
14
14
|
.replace(/\{\{TASK_ID\}\}/g, taskId);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ClientEntry {
|
|
2
|
+
token: string;
|
|
3
|
+
createdAt: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadClients(): ClientEntry[];
|
|
7
|
+
export declare function addClient(label?: string): ClientEntry;
|
|
8
|
+
export declare function revokeClient(token: string): boolean;
|
|
9
|
+
export declare function revokeAllClients(): number;
|
|
10
|
+
export declare function validateClient(token: string): boolean;
|
|
11
|
+
export declare function hasClients(): boolean;
|
|
12
|
+
//# sourceMappingURL=client-store.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
const CLIENTS_FILE = path.join(CONFIG_DIR, "clients.json");
|
|
6
|
+
function readFile() {
|
|
7
|
+
try {
|
|
8
|
+
if (!fs.existsSync(CLIENTS_FILE))
|
|
9
|
+
return [];
|
|
10
|
+
const raw = fs.readFileSync(CLIENTS_FILE, "utf-8");
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function writeFile(clients) {
|
|
18
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
fs.writeFileSync(CLIENTS_FILE, JSON.stringify(clients, null, 2), "utf-8");
|
|
20
|
+
}
|
|
21
|
+
export function loadClients() {
|
|
22
|
+
return readFile();
|
|
23
|
+
}
|
|
24
|
+
export function addClient(label) {
|
|
25
|
+
const clients = readFile();
|
|
26
|
+
const entry = {
|
|
27
|
+
token: randomBytes(32).toString("hex"),
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
...(label ? { label } : {}),
|
|
30
|
+
};
|
|
31
|
+
clients.push(entry);
|
|
32
|
+
writeFile(clients);
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
export function revokeClient(token) {
|
|
36
|
+
const clients = readFile();
|
|
37
|
+
const idx = clients.findIndex((c) => c.token === token);
|
|
38
|
+
if (idx === -1)
|
|
39
|
+
return false;
|
|
40
|
+
clients.splice(idx, 1);
|
|
41
|
+
writeFile(clients);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
export function revokeAllClients() {
|
|
45
|
+
const clients = readFile();
|
|
46
|
+
const count = clients.length;
|
|
47
|
+
writeFile([]);
|
|
48
|
+
return count;
|
|
49
|
+
}
|
|
50
|
+
export function validateClient(token) {
|
|
51
|
+
const clients = readFile();
|
|
52
|
+
return clients.some((c) => c.token === token);
|
|
53
|
+
}
|
|
54
|
+
export function hasClients() {
|
|
55
|
+
return readFile().length > 0;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=client-store.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { loadClients, revokeClient, revokeAllClients } from "../client-store.js";
|
|
2
|
+
export async function clientsListCommand() {
|
|
3
|
+
const clients = loadClients();
|
|
4
|
+
if (clients.length === 0) {
|
|
5
|
+
console.log("No active clients.");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
console.log(`${clients.length} active client(s):\n`);
|
|
9
|
+
for (const c of clients) {
|
|
10
|
+
const label = c.label ? ` (${c.label})` : "";
|
|
11
|
+
console.log(` ${c.token}${label} created ${c.createdAt}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function clientsRevokeCommand(token) {
|
|
15
|
+
if (revokeClient(token)) {
|
|
16
|
+
console.log("Client revoked.");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.error("Client not found.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function clientsRevokeAllCommand() {
|
|
24
|
+
const count = revokeAllClients();
|
|
25
|
+
console.log(`Revoked ${count} client(s).`);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=clients.js.map
|
package/dist/commands/info.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { loadClients } from "../client-store.js";
|
|
3
3
|
/**
|
|
4
4
|
* Print host connection info for setting up clients.
|
|
5
5
|
*/
|
|
6
6
|
export async function infoCommand() {
|
|
7
7
|
const config = loadConfig();
|
|
8
|
-
const
|
|
8
|
+
const clients = loadClients();
|
|
9
9
|
console.log(`Host ID: ${config.hostId}`);
|
|
10
10
|
console.log(`Project root: ${config.projectRoot}`);
|
|
11
11
|
// Detected agents
|
|
@@ -15,9 +15,9 @@ export async function infoCommand() {
|
|
|
15
15
|
else {
|
|
16
16
|
console.log(`Agents: (none detected — run \`palmier agents\`)`);
|
|
17
17
|
}
|
|
18
|
-
//
|
|
19
|
-
console.log(`
|
|
20
|
-
if (
|
|
18
|
+
// Clients
|
|
19
|
+
console.log(`Clients: ${clients.length} active`);
|
|
20
|
+
if (clients.length === 0) {
|
|
21
21
|
console.log("");
|
|
22
22
|
console.log("No paired clients. Run `palmier pair` to connect a device.");
|
|
23
23
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -33,7 +33,7 @@ export async function initCommand() {
|
|
|
33
33
|
// LAN mode
|
|
34
34
|
const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
|
|
35
35
|
const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
|
|
36
|
-
let httpPort =
|
|
36
|
+
let httpPort = 9966;
|
|
37
37
|
const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
|
|
38
38
|
const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
|
|
39
39
|
const parsed = parseInt(portAnswer.trim(), 10);
|
package/dist/commands/pair.js
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
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
7
7
|
const CODE_LENGTH = 6;
|
|
8
8
|
export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
@@ -12,10 +12,10 @@ export function generatePairingCode() {
|
|
|
12
12
|
return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
|
|
13
13
|
}
|
|
14
14
|
function buildPairResponse(config, label) {
|
|
15
|
-
const
|
|
15
|
+
const client = addClient(label);
|
|
16
16
|
return {
|
|
17
17
|
hostId: config.hostId,
|
|
18
|
-
|
|
18
|
+
clientToken: client.token,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -56,7 +56,7 @@ function httpPairRegister(port, code) {
|
|
|
56
56
|
export async function pairCommand() {
|
|
57
57
|
const config = loadConfig();
|
|
58
58
|
const code = generatePairingCode();
|
|
59
|
-
const httpPort = config.httpPort ??
|
|
59
|
+
const httpPort = config.httpPort ?? 9966;
|
|
60
60
|
let paired = false;
|
|
61
61
|
function onPaired() {
|
|
62
62
|
paired = true;
|
package/dist/commands/run.js
CHANGED
|
@@ -39,7 +39,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
39
39
|
console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
|
|
40
40
|
const result = await spawnCommand(command, args, {
|
|
41
41
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
42
|
-
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 ??
|
|
42
|
+
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) },
|
|
43
43
|
echoStdout: true,
|
|
44
44
|
resolveOnFailure: true,
|
|
45
45
|
stdin,
|
|
@@ -248,7 +248,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
248
248
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
249
249
|
const child = spawnStreamingCommand(commandStr, {
|
|
250
250
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
251
|
-
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 ??
|
|
251
|
+
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) },
|
|
252
252
|
});
|
|
253
253
|
let linesProcessed = 0;
|
|
254
254
|
let invocationsSucceeded = 0;
|
|
@@ -365,7 +365,7 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
|
|
|
365
365
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
366
366
|
}
|
|
367
367
|
async function requestPermission(config, task, taskDir, requiredPermissions) {
|
|
368
|
-
const port = config.httpPort ??
|
|
368
|
+
const port = config.httpPort ?? 9966;
|
|
369
369
|
const res = await fetch(`http://localhost:${port}/request-permission`, {
|
|
370
370
|
method: "POST",
|
|
371
371
|
headers: { "Content-Type": "application/json" },
|
|
@@ -387,7 +387,7 @@ async function requestPermission(config, task, taskDir, requiredPermissions) {
|
|
|
387
387
|
return response;
|
|
388
388
|
}
|
|
389
389
|
async function requestConfirmation(config, task, taskDir) {
|
|
390
|
-
const port = config.httpPort ??
|
|
390
|
+
const port = config.httpPort ?? 9966;
|
|
391
391
|
const res = await fetch(`http://localhost:${port}/request-confirmation`, {
|
|
392
392
|
method: "POST",
|
|
393
393
|
headers: { "Content-Type": "application/json" },
|
|
@@ -414,7 +414,8 @@ export function parseReportFiles(output) {
|
|
|
414
414
|
let match;
|
|
415
415
|
while ((match = regex.exec(output)) !== null) {
|
|
416
416
|
const name = match[1].trim();
|
|
417
|
-
|
|
417
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<filename>")
|
|
418
|
+
if (name && !name.startsWith("<"))
|
|
418
419
|
files.push(name);
|
|
419
420
|
}
|
|
420
421
|
return files;
|
|
@@ -429,6 +430,9 @@ export function parsePermissions(output) {
|
|
|
429
430
|
let match;
|
|
430
431
|
while ((match = regex.exec(output)) !== null) {
|
|
431
432
|
const raw = match[1].trim();
|
|
433
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
|
|
434
|
+
if (raw.startsWith("<"))
|
|
435
|
+
continue;
|
|
432
436
|
const sep = raw.indexOf("|");
|
|
433
437
|
if (sep !== -1) {
|
|
434
438
|
perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
|
package/dist/commands/serve.js
CHANGED
|
@@ -99,7 +99,7 @@ export async function serveCommand() {
|
|
|
99
99
|
});
|
|
100
100
|
}, POLL_INTERVAL_MS);
|
|
101
101
|
const handleRpc = createRpcHandler(config, nc);
|
|
102
|
-
const httpPort = config.httpPort ??
|
|
102
|
+
const httpPort = config.httpPort ?? 9966;
|
|
103
103
|
// Start NATS transport (loops forever, fire-and-forget)
|
|
104
104
|
if (nc) {
|
|
105
105
|
startNatsTransport(config, handleRpc, nc);
|
package/dist/events.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function publishHostEvent(nc, hostId, taskId, payload) {
|
|
|
14
14
|
console.log(`[nats] ${subject} →`, payload);
|
|
15
15
|
}
|
|
16
16
|
const config = loadConfig();
|
|
17
|
-
const port = config.httpPort ??
|
|
17
|
+
const port = config.httpPort ?? 9966;
|
|
18
18
|
try {
|
|
19
19
|
await fetch(`http://localhost:${port}/event`, {
|
|
20
20
|
method: "POST",
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { runCommand } from "./commands/run.js";
|
|
|
10
10
|
import { serveCommand } from "./commands/serve.js";
|
|
11
11
|
import { pairCommand } from "./commands/pair.js";
|
|
12
12
|
import { restartCommand } from "./commands/restart.js";
|
|
13
|
-
import {
|
|
13
|
+
import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
|
|
14
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
16
16
|
const program = new Command();
|
|
@@ -54,26 +54,26 @@ program
|
|
|
54
54
|
.action(async () => {
|
|
55
55
|
await pairCommand();
|
|
56
56
|
});
|
|
57
|
-
const
|
|
58
|
-
.command("
|
|
59
|
-
.description("Manage paired
|
|
60
|
-
|
|
57
|
+
const clientsCmd = program
|
|
58
|
+
.command("clients")
|
|
59
|
+
.description("Manage paired clients");
|
|
60
|
+
clientsCmd
|
|
61
61
|
.command("list")
|
|
62
|
-
.description("List active
|
|
62
|
+
.description("List active clients")
|
|
63
63
|
.action(async () => {
|
|
64
|
-
await
|
|
64
|
+
await clientsListCommand();
|
|
65
65
|
});
|
|
66
|
-
|
|
66
|
+
clientsCmd
|
|
67
67
|
.command("revoke <token>")
|
|
68
|
-
.description("Revoke a
|
|
68
|
+
.description("Revoke a client by token")
|
|
69
69
|
.action(async (token) => {
|
|
70
|
-
await
|
|
70
|
+
await clientsRevokeCommand(token);
|
|
71
71
|
});
|
|
72
|
-
|
|
72
|
+
clientsCmd
|
|
73
73
|
.command("revoke-all")
|
|
74
|
-
.description("Revoke all
|
|
74
|
+
.description("Revoke all clients")
|
|
75
75
|
.action(async () => {
|
|
76
|
-
await
|
|
76
|
+
await clientsRevokeAllCommand();
|
|
77
77
|
});
|
|
78
78
|
// No subcommand → default to serve
|
|
79
79
|
if (process.argv.length <= 2) {
|
package/dist/rpc-handler.js
CHANGED
|
@@ -10,7 +10,7 @@ import { getPlatform } from "./platform/index.js";
|
|
|
10
10
|
import { spawnCommand } from "./spawn-command.js";
|
|
11
11
|
import crossSpawn from "cross-spawn";
|
|
12
12
|
import { getAgent } from "./agents/agent.js";
|
|
13
|
-
import {
|
|
13
|
+
import { validateClient } from "./client-store.js";
|
|
14
14
|
import { publishHostEvent } from "./events.js";
|
|
15
15
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
16
16
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
@@ -140,8 +140,8 @@ export function createRpcHandler(config, nc) {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
async function handleRpc(request) {
|
|
143
|
-
//
|
|
144
|
-
if (!request.localhost && (!request.
|
|
143
|
+
// Client token validation: skip for trusted localhost requests
|
|
144
|
+
if (!request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
145
145
|
return { error: "Unauthorized" };
|
|
146
146
|
}
|
|
147
147
|
switch (request.method) {
|
|
@@ -212,8 +212,11 @@ export function createRpcHandler(config, nc) {
|
|
|
212
212
|
existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
213
213
|
if (params.requires_confirmation !== undefined)
|
|
214
214
|
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
215
|
-
if (params.yolo_mode !== undefined)
|
|
215
|
+
if (params.yolo_mode !== undefined) {
|
|
216
216
|
existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
|
|
217
|
+
if (params.yolo_mode)
|
|
218
|
+
delete existing.frontmatter.permissions;
|
|
219
|
+
}
|
|
217
220
|
if (params.command !== undefined) {
|
|
218
221
|
if (params.command) {
|
|
219
222
|
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 } 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";
|
|
@@ -115,9 +115,9 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
115
115
|
const auth = req.headers.authorization;
|
|
116
116
|
if (!auth || !auth.startsWith("Bearer "))
|
|
117
117
|
return false;
|
|
118
|
-
return
|
|
118
|
+
return validateClient(auth.slice(7));
|
|
119
119
|
}
|
|
120
|
-
function
|
|
120
|
+
function extractClientToken(req) {
|
|
121
121
|
const auth = req.headers.authorization;
|
|
122
122
|
if (!auth || !auth.startsWith("Bearer "))
|
|
123
123
|
return undefined;
|
|
@@ -368,11 +368,11 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
368
368
|
sendJson(res, 401, { error: "Invalid code" });
|
|
369
369
|
return;
|
|
370
370
|
}
|
|
371
|
-
const
|
|
371
|
+
const client = addClient(label);
|
|
372
372
|
const ip = detectLanIp();
|
|
373
373
|
const response = {
|
|
374
374
|
hostId: config.hostId,
|
|
375
|
-
|
|
375
|
+
clientToken: client.token,
|
|
376
376
|
directUrl: `http://${ip}:${port}`,
|
|
377
377
|
};
|
|
378
378
|
clearTimeout(pending.timer);
|
|
@@ -449,10 +449,10 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
449
449
|
sendJson(res, 400, { error: "Invalid JSON" });
|
|
450
450
|
return;
|
|
451
451
|
}
|
|
452
|
-
const
|
|
452
|
+
const clientToken = extractClientToken(req);
|
|
453
453
|
console.log(`[http] RPC: ${method}`);
|
|
454
454
|
try {
|
|
455
|
-
const response = await handleRpc({ method, params,
|
|
455
|
+
const response = await handleRpc({ method, params, clientToken, localhost: isLocalhost(req) });
|
|
456
456
|
console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
|
|
457
457
|
sendJson(res, 200, response);
|
|
458
458
|
}
|
|
@@ -39,13 +39,13 @@ export async function startNatsTransport(config, handleRpc, nc) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
// Extract
|
|
43
|
-
const
|
|
44
|
-
delete params.
|
|
42
|
+
// Extract clientToken from params (PWA includes it in the payload)
|
|
43
|
+
const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
|
|
44
|
+
delete params.clientToken;
|
|
45
45
|
console.log(`[nats] RPC: ${method}`);
|
|
46
46
|
let response;
|
|
47
47
|
try {
|
|
48
|
-
response = await handleRpc({ method, params,
|
|
48
|
+
response = await handleRpc({ method, params, clientToken });
|
|
49
49
|
}
|
|
50
50
|
catch (err) {
|
|
51
51
|
console.error(`[nats] RPC error (${method}):`, err);
|
package/dist/types.d.ts
CHANGED
|
@@ -67,8 +67,8 @@ export interface ConversationMessage {
|
|
|
67
67
|
export interface RpcMessage {
|
|
68
68
|
method: string;
|
|
69
69
|
params: Record<string, unknown>;
|
|
70
|
-
|
|
71
|
-
/** Trusted localhost request — skip
|
|
70
|
+
clientToken?: string;
|
|
71
|
+
/** Trusted localhost request — skip client validation. */
|
|
72
72
|
localhost?: boolean;
|
|
73
73
|
}
|
|
74
74
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -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/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\}\}/);
|
|
@@ -38,6 +38,11 @@ describe("parseReportFiles", () => {
|
|
|
38
38
|
it("trims whitespace from file names", () => {
|
|
39
39
|
assert.deepEqual(parseReportFiles("[PALMIER_REPORT] report.md "), ["report.md"]);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
it("ignores placeholder examples from echoed prompt", () => {
|
|
43
|
+
const output = "[PALMIER_REPORT] <filename>\n[PALMIER_REPORT] actual-report.md";
|
|
44
|
+
assert.deepEqual(parseReportFiles(output), ["actual-report.md"]);
|
|
45
|
+
});
|
|
41
46
|
});
|
|
42
47
|
|
|
43
48
|
describe("parsePermissions", () => {
|
|
@@ -57,5 +62,12 @@ describe("parsePermissions", () => {
|
|
|
57
62
|
it("returns empty array when no permissions", () => {
|
|
58
63
|
assert.deepEqual(parsePermissions("no permissions"), []);
|
|
59
64
|
});
|
|
65
|
+
|
|
66
|
+
it("ignores placeholder examples from echoed prompt", () => {
|
|
67
|
+
const output = "[PALMIER_PERMISSION] <tool_name> | <description>\n[PALMIER_PERMISSION] Read | Read files";
|
|
68
|
+
const perms = parsePermissions(output);
|
|
69
|
+
assert.equal(perms.length, 1);
|
|
70
|
+
assert.deepEqual(perms[0], { name: "Read", description: "Read files" });
|
|
71
|
+
});
|
|
60
72
|
});
|
|
61
73
|
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { loadSessions, revokeSession, revokeAllSessions } from "../session-store.js";
|
|
2
|
-
export async function sessionsListCommand() {
|
|
3
|
-
const sessions = loadSessions();
|
|
4
|
-
if (sessions.length === 0) {
|
|
5
|
-
console.log("No active sessions.");
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
console.log(`${sessions.length} active session(s):\n`);
|
|
9
|
-
for (const s of sessions) {
|
|
10
|
-
const label = s.label ? ` (${s.label})` : "";
|
|
11
|
-
console.log(` ${s.token}${label} created ${s.createdAt}`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export async function sessionsRevokeCommand(token) {
|
|
15
|
-
if (revokeSession(token)) {
|
|
16
|
-
console.log("Session revoked.");
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
console.error("Session not found.");
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export async function sessionsRevokeAllCommand() {
|
|
24
|
-
const count = revokeAllSessions();
|
|
25
|
-
console.log(`Revoked ${count} session(s).`);
|
|
26
|
-
}
|
|
27
|
-
//# sourceMappingURL=sessions.js.map
|
package/dist/session-store.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface SessionEntry {
|
|
2
|
-
token: string;
|
|
3
|
-
createdAt: string;
|
|
4
|
-
label?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function loadSessions(): SessionEntry[];
|
|
7
|
-
export declare function addSession(label?: string): SessionEntry;
|
|
8
|
-
export declare function revokeSession(token: string): boolean;
|
|
9
|
-
export declare function revokeAllSessions(): number;
|
|
10
|
-
export declare function validateSession(token: string): boolean;
|
|
11
|
-
export declare function hasSessions(): boolean;
|
|
12
|
-
//# sourceMappingURL=session-store.d.ts.map
|
package/dist/session-store.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
6
|
-
function readFile() {
|
|
7
|
-
try {
|
|
8
|
-
if (!fs.existsSync(SESSIONS_FILE))
|
|
9
|
-
return [];
|
|
10
|
-
const raw = fs.readFileSync(SESSIONS_FILE, "utf-8");
|
|
11
|
-
return JSON.parse(raw);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function writeFile(sessions) {
|
|
18
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
-
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), "utf-8");
|
|
20
|
-
}
|
|
21
|
-
export function loadSessions() {
|
|
22
|
-
return readFile();
|
|
23
|
-
}
|
|
24
|
-
export function addSession(label) {
|
|
25
|
-
const sessions = readFile();
|
|
26
|
-
const entry = {
|
|
27
|
-
token: randomBytes(32).toString("hex"),
|
|
28
|
-
createdAt: new Date().toISOString(),
|
|
29
|
-
...(label ? { label } : {}),
|
|
30
|
-
};
|
|
31
|
-
sessions.push(entry);
|
|
32
|
-
writeFile(sessions);
|
|
33
|
-
return entry;
|
|
34
|
-
}
|
|
35
|
-
export function revokeSession(token) {
|
|
36
|
-
const sessions = readFile();
|
|
37
|
-
const idx = sessions.findIndex((s) => s.token === token);
|
|
38
|
-
if (idx === -1)
|
|
39
|
-
return false;
|
|
40
|
-
sessions.splice(idx, 1);
|
|
41
|
-
writeFile(sessions);
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
export function revokeAllSessions() {
|
|
45
|
-
const sessions = readFile();
|
|
46
|
-
const count = sessions.length;
|
|
47
|
-
writeFile([]);
|
|
48
|
-
return count;
|
|
49
|
-
}
|
|
50
|
-
export function validateSession(token) {
|
|
51
|
-
const sessions = readFile();
|
|
52
|
-
return sessions.some((s) => s.token === token);
|
|
53
|
-
}
|
|
54
|
-
export function hasSessions() {
|
|
55
|
-
return readFile().length > 0;
|
|
56
|
-
}
|
|
57
|
-
//# sourceMappingURL=session-store.js.map
|
package/src/commands/sessions.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { loadSessions, revokeSession, revokeAllSessions } from "../session-store.js";
|
|
2
|
-
|
|
3
|
-
export async function sessionsListCommand(): Promise<void> {
|
|
4
|
-
const sessions = loadSessions();
|
|
5
|
-
if (sessions.length === 0) {
|
|
6
|
-
console.log("No active sessions.");
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
console.log(`${sessions.length} active session(s):\n`);
|
|
11
|
-
for (const s of sessions) {
|
|
12
|
-
const label = s.label ? ` (${s.label})` : "";
|
|
13
|
-
console.log(` ${s.token}${label} created ${s.createdAt}`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function sessionsRevokeCommand(token: string): Promise<void> {
|
|
18
|
-
if (revokeSession(token)) {
|
|
19
|
-
console.log("Session revoked.");
|
|
20
|
-
} else {
|
|
21
|
-
console.error("Session not found.");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function sessionsRevokeAllCommand(): Promise<void> {
|
|
27
|
-
const count = revokeAllSessions();
|
|
28
|
-
console.log(`Revoked ${count} session(s).`);
|
|
29
|
-
}
|
package/src/session-store.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
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 SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
7
|
-
|
|
8
|
-
export interface SessionEntry {
|
|
9
|
-
token: string;
|
|
10
|
-
createdAt: string;
|
|
11
|
-
label?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readFile(): SessionEntry[] {
|
|
15
|
-
try {
|
|
16
|
-
if (!fs.existsSync(SESSIONS_FILE)) return [];
|
|
17
|
-
const raw = fs.readFileSync(SESSIONS_FILE, "utf-8");
|
|
18
|
-
return JSON.parse(raw) as SessionEntry[];
|
|
19
|
-
} catch {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function writeFile(sessions: SessionEntry[]): void {
|
|
25
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
-
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), "utf-8");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function loadSessions(): SessionEntry[] {
|
|
30
|
-
return readFile();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function addSession(label?: string): SessionEntry {
|
|
34
|
-
const sessions = readFile();
|
|
35
|
-
const entry: SessionEntry = {
|
|
36
|
-
token: randomBytes(32).toString("hex"),
|
|
37
|
-
createdAt: new Date().toISOString(),
|
|
38
|
-
...(label ? { label } : {}),
|
|
39
|
-
};
|
|
40
|
-
sessions.push(entry);
|
|
41
|
-
writeFile(sessions);
|
|
42
|
-
return entry;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function revokeSession(token: string): boolean {
|
|
46
|
-
const sessions = readFile();
|
|
47
|
-
const idx = sessions.findIndex((s) => s.token === token);
|
|
48
|
-
if (idx === -1) return false;
|
|
49
|
-
sessions.splice(idx, 1);
|
|
50
|
-
writeFile(sessions);
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function revokeAllSessions(): number {
|
|
55
|
-
const sessions = readFile();
|
|
56
|
-
const count = sessions.length;
|
|
57
|
-
writeFile([]);
|
|
58
|
-
return count;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function validateSession(token: string): boolean {
|
|
62
|
-
const sessions = readFile();
|
|
63
|
-
return sessions.some((s) => s.token === token);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function hasSessions(): boolean {
|
|
67
|
-
return readFile().length > 0;
|
|
68
|
-
}
|