feat(bridge): establish local pi status bridge

Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
This commit is contained in:
2026-07-27 14:51:45 +02:00
commit b7fea83ed6
91 changed files with 15029 additions and 0 deletions
+341
View File
@@ -0,0 +1,341 @@
import path from "node:path";
export const PROTOCOL_VERSION = "v1";
export const MAX_FRAME_BYTES = 64 * 1024;
const requestOperations = new Map([
["list_agents", { agent: false, payload: "none" }],
["list_directories", { agent: false, payload: "none" }],
["select_agent", { agent: false, payload: "worktree" }],
["get_state", { agent: true, payload: "none" }],
["get_session_stats", { agent: true, payload: "none" }],
["list_sessions", { agent: true, payload: "none" }],
["new_session", { agent: true, payload: "none" }],
["switch_session", { agent: true, payload: "session" }],
["get_transcript", { agent: true, payload: "none" }],
["subscribe", { agent: true, payload: "cursor" }],
["prompt", { agent: true, payload: "message" }],
["submit_prompt", { agent: true, payload: "message" }],
["steer", { agent: true, payload: "message" }],
["follow_up", { agent: true, payload: "message" }],
["abort", { agent: true, payload: "none" }],
["extension_response", { agent: true, payload: "extensionResponse" }],
["retry", { agent: true, payload: "none" }],
["restart", { agent: true, payload: "none" }],
["get_available_models", { agent: true, payload: "none" }],
["get_commands", { agent: true, payload: "none" }],
["set_model", { agent: true, payload: "model" }],
["set_thinking_level", { agent: true, payload: "thinking" }],
]);
const eventTypes = new Set([
"agent_state",
"transcript",
"stream",
"tool",
"queue",
"recovery",
"extension_ui_request",
]);
const thinkingLevels = new Set([
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]);
const idPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
export class ProtocolError extends Error {
constructor(code, message) {
super(message);
this.name = "ProtocolError";
this.code = code;
}
}
function isRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function assertRecord(value, field) {
if (!isRecord(value))
throw new ProtocolError("invalid_message", `${field} must be an object`);
return value;
}
function assertAllowedKeys(value, allowed, field) {
for (const key of Object.keys(value)) {
if (!allowed.has(key))
throw new ProtocolError(
"invalid_message",
`${field}.${key} is not supported`,
);
}
}
function assertString(value, field, { maxLength = 4096, pattern } = {}) {
if (
typeof value !== "string" ||
value.length === 0 ||
value.length > maxLength
) {
throw new ProtocolError(
"invalid_message",
`${field} must be a non-empty string up to ${maxLength} characters`,
);
}
if (pattern && !pattern.test(value))
throw new ProtocolError(
"invalid_message",
`${field} has an invalid format`,
);
return value;
}
function assertOptionalCursor(value) {
if (value === undefined) return undefined;
if (Number.isSafeInteger(value) && value >= 0) return value;
return assertString(value, "payload.cursor", {
maxLength: 256,
pattern: idPattern,
});
}
function validatePayload(kind, value) {
if (kind === "none") {
if (value !== undefined)
throw new ProtocolError(
"invalid_message",
"payload is not allowed for this operation",
);
return undefined;
}
const payload = assertRecord(value, "payload");
switch (kind) {
case "worktree": {
assertAllowedKeys(payload, new Set(["worktreePath"]), "payload");
const worktreePath = assertString(
payload.worktreePath,
"payload.worktreePath",
{ maxLength: 4096 },
);
if (!path.isAbsolute(worktreePath))
throw new ProtocolError(
"invalid_message",
"payload.worktreePath must be absolute",
);
return { worktreePath };
}
case "session": {
assertAllowedKeys(payload, new Set(["sessionPath"]), "payload");
const sessionPath = assertString(
payload.sessionPath,
"payload.sessionPath",
{
maxLength: 4096,
},
);
if (!path.isAbsolute(sessionPath))
throw new ProtocolError(
"invalid_message",
"payload.sessionPath must be absolute",
);
return { sessionPath };
}
case "cursor": {
assertAllowedKeys(payload, new Set(["cursor"]), "payload");
return {
...(payload.cursor === undefined
? {}
: { cursor: assertOptionalCursor(payload.cursor) }),
};
}
case "message": {
assertAllowedKeys(payload, new Set(["message"]), "payload");
return {
message: assertString(payload.message, "payload.message", {
maxLength: 32 * 1024,
}),
};
}
case "extensionResponse": {
assertAllowedKeys(payload, new Set(["requestId", "response"]), "payload");
const response = assertRecord(payload.response, "payload.response");
if (
!Object.hasOwn(response, "value") &&
!Object.hasOwn(response, "confirmed") &&
!Object.hasOwn(response, "cancelled")
) {
throw new ProtocolError(
"invalid_message",
"payload.response must contain value, confirmed, or cancelled",
);
}
return {
requestId: assertString(payload.requestId, "payload.requestId", {
maxLength: 128,
pattern: idPattern,
}),
response,
};
}
case "model": {
assertAllowedKeys(payload, new Set(["provider", "modelId"]), "payload");
return {
provider: assertString(payload.provider, "payload.provider", {
maxLength: 128,
}),
modelId: assertString(payload.modelId, "payload.modelId", {
maxLength: 512,
}),
};
}
case "thinking": {
assertAllowedKeys(payload, new Set(["level"]), "payload");
const level = assertString(payload.level, "payload.level", {
maxLength: 16,
});
if (!thinkingLevels.has(level))
throw new ProtocolError(
"invalid_message",
"payload.level is not supported",
);
return { level };
}
default:
throw new ProtocolError("invalid_message", "unsupported payload shape");
}
}
export function validateRequest(value) {
const request = assertRecord(value, "request");
assertAllowedKeys(
request,
new Set(["version", "id", "op", "agentId", "payload"]),
"request",
);
if (request.version !== PROTOCOL_VERSION) {
throw new ProtocolError(
"unsupported_version",
`version must be ${PROTOCOL_VERSION}`,
);
}
const id = assertString(request.id, "request.id", {
maxLength: 128,
pattern: idPattern,
});
const op = assertString(request.op, "request.op", { maxLength: 64 });
const operation = requestOperations.get(op);
if (!operation)
throw new ProtocolError(
"unsupported_operation",
`operation ${op} is not supported`,
);
let agentId;
if (operation.agent) {
agentId = assertString(request.agentId, "request.agentId", {
maxLength: 128,
pattern: idPattern,
});
} else if (request.agentId !== undefined) {
throw new ProtocolError(
"invalid_message",
"request.agentId is not allowed for this operation",
);
}
const payload = validatePayload(operation.payload, request.payload);
return {
version: PROTOCOL_VERSION,
id,
op,
...(agentId ? { agentId } : {}),
...(payload === undefined ? {} : { payload }),
};
}
export function parseRequestFrame(frame, maxFrameBytes = MAX_FRAME_BYTES) {
const bytes = Buffer.isBuffer(frame)
? frame.length
: Buffer.byteLength(frame, "utf8");
if (bytes > maxFrameBytes)
throw new ProtocolError(
"frame_too_large",
`frame exceeds ${maxFrameBytes} bytes`,
);
const line = String(frame).endsWith("\r")
? String(frame).slice(0, -1)
: String(frame);
if (line.length === 0)
throw new ProtocolError("invalid_message", "frame cannot be empty");
let parsed;
try {
parsed = JSON.parse(line);
} catch {
throw new ProtocolError("invalid_json", "frame must contain valid JSON");
}
return validateRequest(parsed);
}
export function validateEvent(value) {
const event = assertRecord(value, "event");
assertAllowedKeys(
event,
new Set(["version", "seq", "type", "agentId", "data"]),
"event",
);
if (event.version !== PROTOCOL_VERSION)
throw new ProtocolError(
"unsupported_version",
`version must be ${PROTOCOL_VERSION}`,
);
if (!Number.isSafeInteger(event.seq) || event.seq < 1)
throw new ProtocolError(
"invalid_message",
"event.seq must be a positive integer",
);
const type = assertString(event.type, "event.type", { maxLength: 64 });
if (!eventTypes.has(type))
throw new ProtocolError(
"unsupported_event",
`event ${type} is not supported`,
);
return {
version: PROTOCOL_VERSION,
seq: event.seq,
type,
agentId: assertString(event.agentId, "event.agentId", {
maxLength: 128,
pattern: idPattern,
}),
data: assertRecord(event.data, "event.data"),
};
}
export function successResponse(id, result = {}) {
return { version: PROTOCOL_VERSION, id, ok: true, result };
}
export function errorResponse(id, error) {
const code = error instanceof ProtocolError ? error.code : "internal_error";
const message =
error instanceof Error ? error.message : "internal bridge error";
return {
version: PROTOCOL_VERSION,
...(id ? { id } : {}),
ok: false,
error: { code, message },
};
}
export function encodeFrame(value) {
return `${JSON.stringify(value)}\n`;
}