@anhlp/alp-protocol 0.0.1-beta.3 → 0.0.1-beta.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/conformance/cases.json +1 -1
- package/dist/connection.d.ts +4 -0
- package/dist/contract-v1.d.ts +4867 -1197
- package/dist/contract-v1.js +1 -1
- package/dist/events.d.ts +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/rpc.d.ts +651 -11
- package/dist/schedule.d.ts +43 -0
- package/dist/schedule.js +0 -0
- package/dist/session.d.ts +142 -2
- package/dist/wire.d.ts +112 -0
- package/dist/wire.js +78 -3
- package/fixtures/v1/cases.json +3247 -0
- package/package.json +1 -1
- package/schema/ipc-v1.json +953 -17
- package/schema/methods-v1.json +4854 -933
- package/src/connection.ts +4 -0
- package/src/contract-v1.js +1 -1
- package/src/events.ts +28 -2
- package/src/index.ts +1 -0
- package/src/rpc.ts +171 -12
- package/src/schedule.ts +43 -0
- package/src/session.ts +120 -4
- package/src/wire.js +128 -2
package/src/wire.js
CHANGED
|
@@ -17,7 +17,7 @@ export const RPC_ERROR = Object.freeze({ parseError: -32700, invalidRequest: -32
|
|
|
17
17
|
* Features a daemon lists in its daemon.hello result. A client uses a feature only when listed;
|
|
18
18
|
* a daemon that lists none (one older than this list) has none of them. Unknown names are ignored.
|
|
19
19
|
*/
|
|
20
|
-
export const CAPABILITY = Object.freeze({ attachAfter: 'attach.after', promptReceipts: 'prompt.receipts', extensionMethods: 'extension.methods', team: 'team', task: 'task', library: 'library', libraryProjectWrite: 'library.project.write', settingsLanguage: 'settings.language', sessionListPage: 'session.list.page', questionWatch: 'question.watch', questionWatchTurns: 'question.watch.turns', workspaceFiles: 'workspace.files', workspaceFilesRead: 'workspace.files.read', promptImages: 'prompt.images' });
|
|
20
|
+
export const CAPABILITY = Object.freeze({ sessionInterruptTurn: 'session.interrupt.turn', attachAfter: 'attach.after', sessionHistory: 'session.history', workspaceRegistry: 'workspace.registry', workspaceWorktree: 'workspace.worktree', workspaceGit: 'workspace.git', promptReceipts: 'prompt.receipts', extensionMethods: 'extension.methods', team: 'team', task: 'task', library: 'library', libraryProjectWrite: 'library.project.write', settingsLanguage: 'settings.language', sessionListPage: 'session.list.page', questionWatch: 'question.watch', questionWatchTurns: 'question.watch.turns', sessionAttention: 'session.attention', workspaceFiles: 'workspace.files', workspaceFilesRead: 'workspace.files.read', promptImages: 'prompt.images', schedule: 'schedule', attachments: 'attachments', sessionFork: 'session.fork', sessionRewind: 'session.rewind' , sessionReload: 'session.reload', sessionResumeCommand: 'session.resumeCommand', sessionConfigureLive: 'session.configure.live', terminal: 'terminal' });
|
|
21
21
|
/**
|
|
22
22
|
* What alpd takes as images in one session.prompt (capability `prompt.images`): inline base64 of
|
|
23
23
|
* these types only, never a path or URL; at most `maxCount` of them, each at most `maxBytes`
|
|
@@ -27,7 +27,7 @@ export const CAPABILITY = Object.freeze({ attachAfter: 'attach.after', promptRec
|
|
|
27
27
|
export const PROMPT_IMAGES = Object.freeze({ mimeTypes: Object.freeze(['image/png', 'image/jpeg', 'image/gif', 'image/webp']), maxCount: 8, maxBytes: 5 * 1024 * 1024, maxTotalBytes: 11 * 1024 * 1024 });
|
|
28
28
|
/** Decoded bytes of base64 `data` (padding aware), without decoding it. @param {string} data */
|
|
29
29
|
export const base64Bytes = data => Math.floor(data.length * 3 / 4) - (data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0);
|
|
30
|
-
export const RPC_METHODS = Object.freeze(["assignment.recall", "board.list", "catalog.get", "daemon.hello", "daemon.pause", "daemon.pauses", "daemon.resume", "daemon.shutdown", "daemon.status", "daemon.usage", "extension.register", "extension.unregister", "library.delete", "library.duplicate", "library.get", "library.list", "library.models", "library.rename", "library.save", "library.skills.set", "question.answer", "question.list", "question.watch", "session.archive", "session.attach", "session.close", "session.configure", "session.create", "session.get", "session.interrupt", "session.list", "session.log", "session.message", "session.preview", "session.prompt", "session.release", "session.rename", "session.status", "settings.language.get", "settings.language.set", "task.create", "task.gate.resolve", "task.list", "task.update", "team.list", "workspace.files.list", "workspace.files.read", "workspace.files.search"]);
|
|
30
|
+
export const RPC_METHODS = Object.freeze(["assignment.recall", "attachment.abort", "attachment.begin", "attachment.chunk", "attachment.commit", "attachment.delete", "attachment.get", "board.list", "catalog.get", "daemon.hello", "daemon.pause", "daemon.pauses", "daemon.resume", "daemon.shutdown", "daemon.status", "daemon.usage", "extension.register", "extension.unregister", "library.delete", "library.duplicate", "library.get", "library.list", "library.models", "library.rename", "library.save", "library.skills.set", "project.list", "project.register", "project.remove", "question.answer", "question.list", "question.watch", "registry.owner", "registry.worktree", "schedule.cancel", "schedule.create", "schedule.get", "schedule.list", "session.archive", "session.attach", "session.attention.clear", "session.close", "session.configure", "session.configure.preview", "session.create", "session.fork", "session.get", "session.history", "session.interrupt", "session.list", "session.log", "session.message", "session.preview", "session.prompt", "session.release", "session.reload", "session.rename", "session.resumeCommand", "session.rewind", "session.status", "settings.language.get", "settings.language.set", "task.create", "task.gate.resolve", "task.list", "task.update", "team.list", "terminal.attach", "terminal.close", "terminal.create", "terminal.input", "terminal.list", "terminal.resize", "terminal.revoke", "workspace.files.list", "workspace.files.read", "workspace.files.search", "workspace.get", "workspace.git.diff", "workspace.git.status", "workspace.list", "workspace.update", "workspace.worktree.create", "workspace.worktree.options", "workspace.worktree.remove"]);
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Limits of workspace.files.list and workspace.files.read (capability `workspace.files.read`,
|
|
@@ -36,6 +36,79 @@ export const RPC_METHODS = Object.freeze(["assignment.recall", "board.list", "ca
|
|
|
36
36
|
*/
|
|
37
37
|
export const WORKSPACE_FILES = Object.freeze({ maxReadBytes: 1024 * 1024, maxListEntries: 1000, maxPathLength: 4096 });
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Limits of workspace.git.status and workspace.git.diff (capability `workspace.git`,
|
|
41
|
+
* docs/protocol/alp-json-rpc-v1.md §12.5): one file's patch at most `maxFileBytes`, one page's
|
|
42
|
+
* patches at most `maxTotalBytes` (then `nextCursor`), at most `maxUntracked` untracked files with
|
|
43
|
+
* content, `maxStatusFiles` status rows, a base branch name of at most `maxRefLength` characters.
|
|
44
|
+
* A gateway may lower them; never raise them.
|
|
45
|
+
*/
|
|
46
|
+
export const WORKSPACE_GIT = Object.freeze({ maxFileBytes: 1024 * 1024, maxTotalBytes: 2 * 1024 * 1024, maxUntracked: 200, maxStatusFiles: 1000, maxRefLength: 255 });
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Limits of non-image attachments (capability `attachments`, docs/protocol/alp-json-rpc-v1.md
|
|
50
|
+
* §12.6): an object of at most `maxObjectBytes`; a prompt of at most `maxPerPrompt` attachments and
|
|
51
|
+
* `maxPromptBytes` together; chunks of at most `maxChunkBytes` decoded (lowered further by
|
|
52
|
+
* attachmentChunkBytes to fit a frame); `maxActiveUploads` incomplete uploads and
|
|
53
|
+
* `maxPrincipalBytes` stored per principal, `maxDaemonBytes` per daemon, retained references
|
|
54
|
+
* included. An incomplete upload expires after `uploadTimeoutMs`; an unreferenced committed object
|
|
55
|
+
* is purged after `unreferencedRetentionMs`, one whose references were all released after
|
|
56
|
+
* `releasedGraceMs`. A display name is at most `maxNameBytes` UTF-8 bytes. An operator or a gateway
|
|
57
|
+
* may lower them, never raise them; daemon.hello advertises the effective ones (`attachmentLimits`).
|
|
58
|
+
*/
|
|
59
|
+
export const ATTACHMENTS = Object.freeze({
|
|
60
|
+
maxObjectBytes: 20 * 1024 * 1024,
|
|
61
|
+
maxPerPrompt: 8,
|
|
62
|
+
maxPromptBytes: 40 * 1024 * 1024,
|
|
63
|
+
maxChunkBytes: 256 * 1024,
|
|
64
|
+
maxActiveUploads: 2,
|
|
65
|
+
maxPrincipalBytes: 200 * 1024 * 1024,
|
|
66
|
+
maxDaemonBytes: 1024 * 1024 * 1024,
|
|
67
|
+
uploadTimeoutMs: 60 * 60 * 1000,
|
|
68
|
+
unreferencedRetentionMs: 24 * 60 * 60 * 1000,
|
|
69
|
+
releasedGraceMs: 7 * 24 * 60 * 60 * 1000,
|
|
70
|
+
maxNameBytes: 255,
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Limits of terminals (capability `terminal`, docs/protocol/alp-json-rpc-v1.md §12.10): at most
|
|
74
|
+
* `maxPerPrincipal` running terminals per principal and `maxPerDaemon` per daemon; a
|
|
75
|
+
* terminal.input of at most `maxInputBytes` UTF-8 bytes; output events of at most
|
|
76
|
+
* `maxOutputFrameBytes` bytes of data; `replayBytes` of output kept per terminal for attach; a
|
|
77
|
+
* terminal ends after `idleMs` without input or output, after `maxLifetimeMs` in any case, and
|
|
78
|
+
* `reconnectMs` after its stream detached unless attached again. Sizes are `minCols`..`maxCols`
|
|
79
|
+
* columns and `minRows`..`maxRows` rows. An operator or a gateway may lower the first eight, never
|
|
80
|
+
* raise them; daemon.hello advertises the effective ones (`terminalLimits`).
|
|
81
|
+
*/
|
|
82
|
+
export const TERMINAL = Object.freeze({
|
|
83
|
+
maxPerPrincipal: 4,
|
|
84
|
+
maxPerDaemon: 16,
|
|
85
|
+
maxInputBytes: 64 * 1024,
|
|
86
|
+
maxOutputFrameBytes: 64 * 1024,
|
|
87
|
+
replayBytes: 1024 * 1024,
|
|
88
|
+
idleMs: 30 * 60 * 1000,
|
|
89
|
+
maxLifetimeMs: 8 * 60 * 60 * 1000,
|
|
90
|
+
reconnectMs: 60 * 1000,
|
|
91
|
+
minCols: 2,
|
|
92
|
+
maxCols: 500,
|
|
93
|
+
minRows: 1,
|
|
94
|
+
maxRows: 200,
|
|
95
|
+
});
|
|
96
|
+
/** The TERMINAL limits a daemon.hello `terminalLimits` carries (the size bounds are fixed). */
|
|
97
|
+
export const TERMINAL_LIMIT_KEYS = Object.freeze(['maxPerPrincipal', 'maxPerDaemon', 'maxInputBytes', 'maxOutputFrameBytes', 'replayBytes', 'idleMs', 'maxLifetimeMs', 'reconnectMs']);
|
|
98
|
+
/** An opaque terminal id (§12.10). @param {unknown} id */
|
|
99
|
+
export const isTerminalId = id => typeof id === 'string' && /^term_[0-9a-f]{32}$/.test(id);
|
|
100
|
+
/** Room an attachment.chunk request needs besides its base64 data. */
|
|
101
|
+
export const ATTACHMENT_CHUNK_OVERHEAD = 64 * 1024;
|
|
102
|
+
/**
|
|
103
|
+
* The decoded bytes one attachment.chunk may carry over frames of at most `maxFrame` bytes:
|
|
104
|
+
* `maxChunkBytes`, lowered so its base64 and ATTACHMENT_CHUNK_OVERHEAD fit. At least 1 byte.
|
|
105
|
+
* @param {number} [maxFrame] @param {number} [maxChunkBytes]
|
|
106
|
+
*/
|
|
107
|
+
export function attachmentChunkBytes(maxFrame = MAX_FRAME, maxChunkBytes = ATTACHMENTS.maxChunkBytes) {
|
|
108
|
+
const fits = Math.floor((maxFrame - ATTACHMENT_CHUNK_OVERHEAD) * 3 / 4);
|
|
109
|
+
return Math.max(1, Math.min(maxChunkBytes, fits));
|
|
110
|
+
}
|
|
111
|
+
|
|
39
112
|
/** Directories whose whole content is refused: Git's, ALP's own configuration (its library overrides hold env values), and key and cloud-credential stores. */
|
|
40
113
|
const BLOCKED_DIRECTORIES = new Set(['.git', '.alp', '.ssh', '.gnupg', '.aws', '.azure', '.kube', '.docker', 'gcloud']);
|
|
41
114
|
/** Files that hold credentials by convention. */
|
|
@@ -85,3 +158,56 @@ export function blockedFilePath(relative) {
|
|
|
85
158
|
export function encodeFrame(value) { return `${JSON.stringify(value)}\n`; }
|
|
86
159
|
/** Parse one complete frame. Buffering and failure handling belong to the socket owner. @param {string} line @returns {unknown} */
|
|
87
160
|
export function decodeFrame(line) { return JSON.parse(line); }
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The command lines of session.resumeCommand (docs/protocol/alp-json-rpc-v1.md §12.8), built from argv
|
|
164
|
+
* so that every argument reaches the program as it is: for a POSIX shell (sh, bash, zsh) and for
|
|
165
|
+
* Windows cmd.exe, typed at its prompt and then read again by npm's `alp.cmd` shim (`%*`). An argument
|
|
166
|
+
* with a control character (a line break would run a pasted command early) is refused, and for cmd.exe
|
|
167
|
+
* one with `"`, `%` or `!` too: a quote inside an argument ends the shim's quoting and lets the rest
|
|
168
|
+
* run, and `%`/`!` expand variables in either parse, so no escaping keeps them (session ids have none).
|
|
169
|
+
*/
|
|
170
|
+
const POSIX_SAFE = /^[A-Za-z0-9_@+:,./-]+$/;
|
|
171
|
+
const WINDOWS_SAFE = /^[A-Za-z0-9_@+:./\\-]+$/;
|
|
172
|
+
const assertArgument = (/** @type {unknown} */ arg) => {
|
|
173
|
+
if (typeof arg !== 'string') throw new TypeError('A command argument is a string');
|
|
174
|
+
if (/[\u0000-\u001f\u007f]/.test(arg)) throw new TypeError('A command argument has no control characters');
|
|
175
|
+
return arg;
|
|
176
|
+
};
|
|
177
|
+
/** One argument for a POSIX shell: as it is when nothing in it is special, else in single quotes. @param {string} arg */
|
|
178
|
+
export function quotePosix(arg) {
|
|
179
|
+
const text = assertArgument(arg);
|
|
180
|
+
return text && POSIX_SAFE.test(text) ? text : `'${text.replace(/'/g, `'\\''`)}'`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* One argument for cmd.exe: as it is when nothing in it is special; else in double quotes for the C
|
|
184
|
+
* runtime's split (backslashes before the closing quote doubled), with every cmd.exe metacharacter,
|
|
185
|
+
* the quotes too, escaped with ^ for the interactive parse; the shim's second parse then sees one
|
|
186
|
+
* quoted argument, inside which `&|<>()^` are literal. `"`, `%` and `!` are refused (see above).
|
|
187
|
+
* @param {string} arg
|
|
188
|
+
*/
|
|
189
|
+
export function quoteWindows(arg) {
|
|
190
|
+
const text = assertArgument(arg);
|
|
191
|
+
if (text && WINDOWS_SAFE.test(text)) return text;
|
|
192
|
+
if (/["%!]/.test(text)) throw new TypeError('A cmd.exe command argument has no ", % or !');
|
|
193
|
+
const quoted = `"${text.replace(/(\\*)$/, '$1$1')}"`;
|
|
194
|
+
return quoted.replace(/([()\][^"`<>&|;, *?])/g, '^$1');
|
|
195
|
+
}
|
|
196
|
+
const SESSION_ID = /^[\w.:-]{1,128}$/;
|
|
197
|
+
/** argv as one command line. @param {string[]} argv @param {'posix' | 'windows'} platform */
|
|
198
|
+
export function commandLine(argv, platform) {
|
|
199
|
+
return argv.map(platform === 'windows' ? quoteWindows : quotePosix).join(' ');
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* session.resumeCommand's answer for a session: `alp attach <sessionId>`, which follows the session in
|
|
203
|
+
* a terminal (a closed one says how to send it a message). ALP keeps the orchestration, so it is never
|
|
204
|
+
* a native runtime's own resume command.
|
|
205
|
+
* @param {string} sessionId @param {'posix' | 'windows'} platform the daemon host's
|
|
206
|
+
*/
|
|
207
|
+
export function resumeCommandOf(sessionId, platform) {
|
|
208
|
+
if (typeof sessionId !== 'string' || !SESSION_ID.test(sessionId)) throw new TypeError('A session id is [A-Za-z0-9_.:-], at most 128 characters');
|
|
209
|
+
const argv = ['alp', 'attach', sessionId];
|
|
210
|
+
const posix = commandLine(argv, 'posix');
|
|
211
|
+
const windows = commandLine(argv, 'windows');
|
|
212
|
+
return { argv, posix, windows, display: platform === 'windows' ? windows : posix, platform };
|
|
213
|
+
}
|