@pushary/openclaw 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +112 -2
- package/assets/icon.png +0 -0
- package/dist/api-key-shape.d.ts +3 -0
- package/dist/api-key-shape.js +5 -0
- package/dist/approval-hook.d.ts +23 -0
- package/dist/approval-hook.js +88 -0
- package/dist/approval-question.d.ts +19 -0
- package/dist/approval-question.js +66 -0
- package/dist/connection.d.ts +26 -0
- package/dist/connection.js +101 -0
- package/dist/decision-reply.d.ts +9 -0
- package/dist/decision-reply.js +6 -0
- package/dist/gate-result.d.ts +13 -0
- package/dist/gate-result.js +58 -0
- package/dist/gate.d.ts +49 -0
- package/dist/gate.js +173 -0
- package/dist/hook-budget.d.ts +10 -0
- package/dist/hook-budget.js +19 -0
- package/dist/host-api.d.ts +60 -0
- package/dist/host-api.js +1 -0
- package/dist/host-version.d.ts +12 -0
- package/dist/host-version.js +25 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +32 -0
- package/dist/openclaw-paths.d.ts +14 -0
- package/dist/openclaw-paths.js +43 -0
- package/dist/openclaw-write-guard.d.ts +5 -0
- package/dist/openclaw-write-guard.js +93 -0
- package/dist/protected-writes.d.ts +33 -0
- package/dist/protected-writes.js +217 -0
- package/dist/pushary-client.d.ts +42 -0
- package/dist/pushary-client.js +103 -0
- package/dist/pushary-hosts.d.ts +2 -0
- package/dist/pushary-hosts.js +3 -0
- package/dist/redaction.d.ts +9 -0
- package/dist/redaction.js +53 -0
- package/dist/replies.d.ts +33 -0
- package/dist/replies.js +65 -0
- package/dist/settings.d.ts +34 -0
- package/dist/settings.js +103 -0
- package/openclaw.plugin.json +32 -0
- package/package.json +74 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const WRITE_TOOL = 'write';
|
|
5
|
+
export const EDIT_TOOL = 'edit';
|
|
6
|
+
export const APPLY_PATCH_TOOL = 'apply_patch';
|
|
7
|
+
export const FILE_WRITING_TOOLS = [WRITE_TOOL, EDIT_TOOL, APPLY_PATCH_TOOL];
|
|
8
|
+
const PATH_PARAMETER = 'path';
|
|
9
|
+
const PATCH_PARAMETER = 'input';
|
|
10
|
+
const PATCH_TARGET_MARKERS = ['*** Add File: ', '*** Delete File: ', '*** Update File: ', '*** Move to: '];
|
|
11
|
+
const PATCH_MARKER_PREFIX = '***';
|
|
12
|
+
const FILE_URL_PREFIX = 'file://';
|
|
13
|
+
const FILE_REFERENCE_PREFIX = '@';
|
|
14
|
+
const HOME_PREFIX = '~';
|
|
15
|
+
const MISSING_PATH_CODES = new Set(['ENOENT', 'ENOTDIR']);
|
|
16
|
+
const WORKSPACE_HOOKS_DIRECTORY = 'hooks';
|
|
17
|
+
const RISKY_RELATIVE_STARTS = ['.pushary', '.config'];
|
|
18
|
+
const errorCodeOf = (error) => typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : null;
|
|
19
|
+
const canonicalPathOf = (path, system) => {
|
|
20
|
+
const missing = [];
|
|
21
|
+
let current = normalize(path);
|
|
22
|
+
for (;;) {
|
|
23
|
+
try {
|
|
24
|
+
const real = system.realpath(current);
|
|
25
|
+
return missing.length === 0 ? real : join(real, ...missing.reverse());
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = errorCodeOf(error);
|
|
29
|
+
const parent = dirname(current);
|
|
30
|
+
if (code === null || !MISSING_PATH_CODES.has(code) || parent === current)
|
|
31
|
+
return null;
|
|
32
|
+
missing.push(basename(current));
|
|
33
|
+
current = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const comparable = (path, system) => {
|
|
38
|
+
const composed = path.normalize('NFC');
|
|
39
|
+
return system.caseInsensitive ? composed.toLowerCase() : composed;
|
|
40
|
+
};
|
|
41
|
+
const spellingsOfLocation = (path, system) => {
|
|
42
|
+
const canonical = canonicalPathOf(path, system);
|
|
43
|
+
const resolved = comparable(resolve(path), system);
|
|
44
|
+
return canonical === null ? [resolved] : [...new Set([comparable(canonical, system), resolved])];
|
|
45
|
+
};
|
|
46
|
+
const spellingsOfAll = (paths, system) => [...new Set(paths.flatMap(path => spellingsOfLocation(path, system)))];
|
|
47
|
+
const isSameOrInside = (path, directory) => path === directory || path.startsWith(directory.endsWith(sep) ? directory : `${directory}${sep}`);
|
|
48
|
+
const isStrictlyInside = (path, directory) => path !== directory && isSameOrInside(path, directory);
|
|
49
|
+
const firstComponentUnder = (path, directory) => {
|
|
50
|
+
if (!isStrictlyInside(path, directory))
|
|
51
|
+
return null;
|
|
52
|
+
return relative(directory, path).split(sep)[0] ?? null;
|
|
53
|
+
};
|
|
54
|
+
const comparablePathsFor = (locations, system, agentId) => {
|
|
55
|
+
const stateDirectories = spellingsOfAll(locations.stateDirectories, system);
|
|
56
|
+
const workspace = system.workspaceFor(agentId);
|
|
57
|
+
const workspaceCanonical = workspace === null ? null : canonicalPathOf(workspace, system);
|
|
58
|
+
return {
|
|
59
|
+
stateDirectories,
|
|
60
|
+
configFiles: spellingsOfAll(locations.configFiles, system),
|
|
61
|
+
protectedTrees: spellingsOfAll(locations.protectedTrees, system),
|
|
62
|
+
serviceFiles: locations.serviceFiles.flatMap(location => spellingsOfLocation(location.directory, system).map(directory => ({ directory, namePrefix: comparable(location.namePrefix, system) }))),
|
|
63
|
+
workspace: workspaceCanonical === null ? null : comparable(workspaceCanonical, system),
|
|
64
|
+
riskyRelativeStarts: new Set([
|
|
65
|
+
...locations.stateDirectories.map(state => comparable(basename(state), system)),
|
|
66
|
+
...locations.configFiles.map(file => comparable(basename(file), system)),
|
|
67
|
+
...RISKY_RELATIVE_STARTS.map(start => comparable(start, system)),
|
|
68
|
+
]),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const isServiceFile = (path, services) => services.some(service => firstComponentUnder(path, service.directory)?.startsWith(service.namePrefix) === true);
|
|
72
|
+
const isInWorkspaceHooks = (path, workspace) => workspace !== null && firstComponentUnder(path, workspace) === WORKSPACE_HOOKS_DIRECTORY;
|
|
73
|
+
const isInWorkspaceUnderState = (path, paths) => {
|
|
74
|
+
const { workspace } = paths;
|
|
75
|
+
if (workspace === null || !isSameOrInside(path, workspace))
|
|
76
|
+
return false;
|
|
77
|
+
return paths.stateDirectories.some(state => isStrictlyInside(workspace, state));
|
|
78
|
+
};
|
|
79
|
+
const isProtectedLocation = (path, paths) => {
|
|
80
|
+
if (paths.configFiles.includes(path))
|
|
81
|
+
return true;
|
|
82
|
+
if (paths.protectedTrees.some(tree => isSameOrInside(path, tree)))
|
|
83
|
+
return true;
|
|
84
|
+
if (isServiceFile(path, paths.serviceFiles))
|
|
85
|
+
return true;
|
|
86
|
+
if (isInWorkspaceHooks(path, paths.workspace))
|
|
87
|
+
return true;
|
|
88
|
+
if (!paths.stateDirectories.some(state => isSameOrInside(path, state)))
|
|
89
|
+
return false;
|
|
90
|
+
return !isInWorkspaceUnderState(path, paths);
|
|
91
|
+
};
|
|
92
|
+
const verdictForAbsolute = (path, paths, system) => {
|
|
93
|
+
const canonical = canonicalPathOf(path, system);
|
|
94
|
+
if (canonical === null)
|
|
95
|
+
return 'uncertain';
|
|
96
|
+
return isProtectedLocation(comparable(canonical, system), paths) ? 'protected' : 'clear';
|
|
97
|
+
};
|
|
98
|
+
const firstComponentOf = (relativePath) => normalize(relativePath).split(/[\\/]/).find(part => part.length > 0) ?? '';
|
|
99
|
+
const expandedHome = (path, osHome) => path === HOME_PREFIX || path.startsWith(`${HOME_PREFIX}/`) || (process.platform === 'win32' && path.startsWith(`${HOME_PREFIX}\\`))
|
|
100
|
+
? join(osHome, path.slice(HOME_PREFIX.length))
|
|
101
|
+
: path;
|
|
102
|
+
const spellingsOf = (raw) => {
|
|
103
|
+
if (!raw.startsWith(FILE_REFERENCE_PREFIX))
|
|
104
|
+
return [raw];
|
|
105
|
+
const referenced = raw.slice(FILE_REFERENCE_PREFIX.length);
|
|
106
|
+
return [referenced.startsWith(FILE_REFERENCE_PREFIX) ? `./${referenced}` : referenced, `./${raw}`];
|
|
107
|
+
};
|
|
108
|
+
const absoluteOrRelative = (spelling, osHome) => {
|
|
109
|
+
if (!spelling.startsWith(FILE_URL_PREFIX))
|
|
110
|
+
return expandedHome(spelling, osHome);
|
|
111
|
+
try {
|
|
112
|
+
return fileURLToPath(spelling);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const worstOf = (verdicts) => {
|
|
119
|
+
if (verdicts.includes('protected'))
|
|
120
|
+
return 'protected';
|
|
121
|
+
if (verdicts.length === 0 || verdicts.includes('uncertain'))
|
|
122
|
+
return 'uncertain';
|
|
123
|
+
return 'clear';
|
|
124
|
+
};
|
|
125
|
+
const PARENT_SEGMENT = '..';
|
|
126
|
+
const leadingClimbsOf = (relativePath) => {
|
|
127
|
+
const parts = normalize(relativePath).split(/[\\/]/);
|
|
128
|
+
const firstOther = parts.findIndex(part => part !== PARENT_SEGMENT);
|
|
129
|
+
return firstOther === -1 ? parts.length : firstOther;
|
|
130
|
+
};
|
|
131
|
+
const ancestorsUpTo = (directory, levels) => {
|
|
132
|
+
const ancestors = [directory];
|
|
133
|
+
for (let level = 0; level < levels; level += 1)
|
|
134
|
+
ancestors.push(dirname(ancestors[ancestors.length - 1] ?? directory));
|
|
135
|
+
return ancestors;
|
|
136
|
+
};
|
|
137
|
+
const landingPlacesOf = (relativePath, workspace, others) => {
|
|
138
|
+
const climbs = leadingClimbsOf(relativePath);
|
|
139
|
+
const rest = normalize(relativePath).split(/[\\/]/).slice(climbs).join(sep);
|
|
140
|
+
const fromInsideTheWorkspace = ancestorsUpTo(workspace, climbs).map(base => join(base, rest));
|
|
141
|
+
return [...fromInsideTheWorkspace, resolve(workspace, relativePath), ...others.map(directory => resolve(directory, relativePath))];
|
|
142
|
+
};
|
|
143
|
+
const verdictForSpelling = (spelling, paths, system) => {
|
|
144
|
+
const path = absoluteOrRelative(spelling, system.osHome);
|
|
145
|
+
if (path === null || path.trim().length === 0)
|
|
146
|
+
return 'uncertain';
|
|
147
|
+
if (isAbsolute(path))
|
|
148
|
+
return verdictForAbsolute(path, paths, system);
|
|
149
|
+
if (paths.riskyRelativeStarts.has(comparable(firstComponentOf(path), system)))
|
|
150
|
+
return 'protected';
|
|
151
|
+
if (paths.workspace === null)
|
|
152
|
+
return 'uncertain';
|
|
153
|
+
return worstOf(landingPlacesOf(path, paths.workspace, system.otherWorkingDirectories).map(place => verdictForAbsolute(place, paths, system)));
|
|
154
|
+
};
|
|
155
|
+
const verdictForRawPath = (raw, paths, system) => {
|
|
156
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
157
|
+
return 'uncertain';
|
|
158
|
+
return worstOf(spellingsOf(raw.trim()).map(spelling => verdictForSpelling(spelling, paths, system)));
|
|
159
|
+
};
|
|
160
|
+
const patchTargetOf = (line) => {
|
|
161
|
+
const header = line.trimStart();
|
|
162
|
+
if (!header.startsWith(PATCH_MARKER_PREFIX))
|
|
163
|
+
return null;
|
|
164
|
+
const trimmed = header.trimEnd();
|
|
165
|
+
const marker = PATCH_TARGET_MARKERS.find(candidate => trimmed.startsWith(candidate));
|
|
166
|
+
return marker === undefined ? null : trimmed.slice(marker.length);
|
|
167
|
+
};
|
|
168
|
+
export const patchTargetsOf = (patch) => typeof patch === 'string' ? patch.split(/\r?\n/).flatMap(line => {
|
|
169
|
+
const target = patchTargetOf(line);
|
|
170
|
+
return target === null ? [] : [target];
|
|
171
|
+
}) : [];
|
|
172
|
+
const hostResolvedPathsOf = (event) => Array.isArray(event.derivedPaths) ? event.derivedPaths : [];
|
|
173
|
+
const rawTargetsOf = (toolId, event) => {
|
|
174
|
+
if (toolId === APPLY_PATCH_TOOL) {
|
|
175
|
+
const targets = [...patchTargetsOf(event.params[PATCH_PARAMETER]), ...hostResolvedPathsOf(event)];
|
|
176
|
+
return targets.length === 0 ? null : targets;
|
|
177
|
+
}
|
|
178
|
+
return [event.params[PATH_PARAMETER]];
|
|
179
|
+
};
|
|
180
|
+
export const writesIntoOpenClaw = (locations, system, call) => {
|
|
181
|
+
if (!FILE_WRITING_TOOLS.includes(call.toolId))
|
|
182
|
+
return false;
|
|
183
|
+
const targets = rawTargetsOf(call.toolId, call.event);
|
|
184
|
+
if (targets === null)
|
|
185
|
+
return true;
|
|
186
|
+
const paths = comparablePathsFor(locations, system, call.agentId);
|
|
187
|
+
return targets.some(target => verdictForRawPath(target, paths, system) !== 'clear');
|
|
188
|
+
};
|
|
189
|
+
export const SESSIONS_SPAWN_TOOL = 'sessions_spawn';
|
|
190
|
+
export const SPAWN_FOLDER_PARAMETER = 'cwd';
|
|
191
|
+
const SPAWN_SOURCES_IT_CANNOT_SEE = ['projectId', 'projectGitUrl', 'worktree'];
|
|
192
|
+
const namesAFolderItCannotSee = (params) => SPAWN_SOURCES_IT_CANNOT_SEE.some(name => params[name] !== undefined && params[name] !== false);
|
|
193
|
+
const spawnFolderVerdict = (raw, paths, system) => {
|
|
194
|
+
if (typeof raw !== 'string' || raw.trim().length === 0 || paths.workspace === null)
|
|
195
|
+
return 'uncertain';
|
|
196
|
+
const expanded = absoluteOrRelative(raw.trim(), system.osHome);
|
|
197
|
+
if (expanded === null)
|
|
198
|
+
return 'uncertain';
|
|
199
|
+
const canonical = canonicalPathOf(resolve(paths.workspace, expanded), system);
|
|
200
|
+
if (canonical === null)
|
|
201
|
+
return 'uncertain';
|
|
202
|
+
const folder = comparable(canonical, system);
|
|
203
|
+
if (!isSameOrInside(folder, paths.workspace))
|
|
204
|
+
return 'protected';
|
|
205
|
+
return isInWorkspaceHooks(folder, paths.workspace) ? 'protected' : 'clear';
|
|
206
|
+
};
|
|
207
|
+
export const spawnsOutsideTheWorkspace = (locations, system, call) => {
|
|
208
|
+
if (call.toolId !== SESSIONS_SPAWN_TOOL)
|
|
209
|
+
return false;
|
|
210
|
+
const { params } = call.event;
|
|
211
|
+
if (namesAFolderItCannotSee(params))
|
|
212
|
+
return true;
|
|
213
|
+
if (params[SPAWN_FOLDER_PARAMETER] === undefined)
|
|
214
|
+
return false;
|
|
215
|
+
return spawnFolderVerdict(params[SPAWN_FOLDER_PARAMETER], comparablePathsFor(locations, system, call.agentId), system) !== 'clear';
|
|
216
|
+
};
|
|
217
|
+
export const nodeRealpath = (path) => realpathSync(path);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PusharyConnection } from './connection.js';
|
|
2
|
+
export declare const MCP_PATH = "/api/mcp/mcp";
|
|
3
|
+
export declare const ASK_USER_TOOL = "ask_user";
|
|
4
|
+
export declare const WAIT_FOR_ANSWER_TOOL = "wait_for_answer";
|
|
5
|
+
export declare const CANCEL_QUESTION_TOOL = "cancel_question";
|
|
6
|
+
export declare const ASK_REQUEST_TIMEOUT_MS = 10000;
|
|
7
|
+
export declare const CANCEL_REQUEST_TIMEOUT_MS = 4000;
|
|
8
|
+
export declare const POLL_TIMEOUT_MIN_MS = 1000;
|
|
9
|
+
export declare const POLL_TIMEOUT_MAX_MS = 30000;
|
|
10
|
+
export declare const POLL_RESPONSE_GRACE_MS = 2000;
|
|
11
|
+
export type CallFailure = 'aborted' | 'timed_out' | 'network' | 'unauthorized' | 'refused' | 'server_error' | 'unreadable' | 'tool_error';
|
|
12
|
+
export type CallResult = {
|
|
13
|
+
readonly kind: 'reply';
|
|
14
|
+
readonly value: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'failed';
|
|
17
|
+
readonly failure: CallFailure;
|
|
18
|
+
};
|
|
19
|
+
export interface AskUserArguments {
|
|
20
|
+
readonly question: string;
|
|
21
|
+
readonly type: 'confirm';
|
|
22
|
+
readonly wait: false;
|
|
23
|
+
readonly context: string;
|
|
24
|
+
readonly agentName: string;
|
|
25
|
+
readonly toolName: string;
|
|
26
|
+
readonly action: string;
|
|
27
|
+
readonly actionBody: string;
|
|
28
|
+
readonly waitEndsAt: string;
|
|
29
|
+
readonly localSurfaceAvailable: false;
|
|
30
|
+
readonly toolTarget?: string;
|
|
31
|
+
readonly sessionId?: string;
|
|
32
|
+
readonly machineId?: string;
|
|
33
|
+
readonly requestId: string;
|
|
34
|
+
}
|
|
35
|
+
export interface PusharyQuestions {
|
|
36
|
+
readonly ask: (args: AskUserArguments) => Promise<CallResult>;
|
|
37
|
+
readonly waitForAnswer: (correlationId: string, timeoutMs: number, signal: AbortSignal | undefined) => Promise<CallResult>;
|
|
38
|
+
readonly cancel: (correlationId: string) => Promise<CallResult>;
|
|
39
|
+
}
|
|
40
|
+
export type Fetcher = (input: string, init: RequestInit) => Promise<Response>;
|
|
41
|
+
export declare const clampedPollMs: (timeoutMs: number) => number;
|
|
42
|
+
export declare const createPusharyQuestions: (connection: PusharyConnection, fetcher?: Fetcher) => PusharyQuestions;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const MCP_PATH = '/api/mcp/mcp';
|
|
2
|
+
export const ASK_USER_TOOL = 'ask_user';
|
|
3
|
+
export const WAIT_FOR_ANSWER_TOOL = 'wait_for_answer';
|
|
4
|
+
export const CANCEL_QUESTION_TOOL = 'cancel_question';
|
|
5
|
+
export const ASK_REQUEST_TIMEOUT_MS = 10_000;
|
|
6
|
+
export const CANCEL_REQUEST_TIMEOUT_MS = 4_000;
|
|
7
|
+
export const POLL_TIMEOUT_MIN_MS = 1_000;
|
|
8
|
+
export const POLL_TIMEOUT_MAX_MS = 30_000;
|
|
9
|
+
export const POLL_RESPONSE_GRACE_MS = 2_000;
|
|
10
|
+
const JSON_RPC_VERSION = '2.0';
|
|
11
|
+
const TOOLS_CALL_METHOD = 'tools/call';
|
|
12
|
+
const EVENT_STREAM_TYPE = 'text/event-stream';
|
|
13
|
+
const EVENT_DATA_PREFIX = 'data:';
|
|
14
|
+
const UNAUTHORIZED_STATUSES = new Set([401, 403]);
|
|
15
|
+
const TOO_MANY_REQUESTS_STATUS = 429;
|
|
16
|
+
const SERVER_ERROR_STATUS_FLOOR = 500;
|
|
17
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
const parsedJson = (text) => {
|
|
19
|
+
try {
|
|
20
|
+
return { ok: true, value: JSON.parse(text) };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { ok: false };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const lastEventData = (stream) => {
|
|
27
|
+
const events = stream.split(/\r?\n\r?\n/).map(event => event.split(/\r?\n/).filter(line => line.startsWith(EVENT_DATA_PREFIX)).map(line => line.slice(EVENT_DATA_PREFIX.length).trimStart()).join('\n').trim());
|
|
28
|
+
return events.filter(data => data.length > 0).at(-1) ?? null;
|
|
29
|
+
};
|
|
30
|
+
const envelopeOf = (body, contentType) => {
|
|
31
|
+
const text = contentType.includes(EVENT_STREAM_TYPE) ? lastEventData(body) : body;
|
|
32
|
+
if (text === null || text.trim().length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const parsed = parsedJson(text);
|
|
35
|
+
return parsed.ok ? parsed.value : null;
|
|
36
|
+
};
|
|
37
|
+
const firstTextContent = (content) => {
|
|
38
|
+
if (!Array.isArray(content))
|
|
39
|
+
return null;
|
|
40
|
+
const first = content[0];
|
|
41
|
+
return isRecord(first) && typeof first.text === 'string' ? first.text : null;
|
|
42
|
+
};
|
|
43
|
+
const toolReplyOf = (envelope) => {
|
|
44
|
+
if (!isRecord(envelope))
|
|
45
|
+
return { kind: 'failed', failure: 'unreadable' };
|
|
46
|
+
if (envelope.error !== undefined)
|
|
47
|
+
return { kind: 'failed', failure: 'tool_error' };
|
|
48
|
+
const result = envelope.result;
|
|
49
|
+
if (!isRecord(result))
|
|
50
|
+
return { kind: 'failed', failure: 'unreadable' };
|
|
51
|
+
if (result.isError === true)
|
|
52
|
+
return { kind: 'failed', failure: 'tool_error' };
|
|
53
|
+
if (isRecord(result.structuredContent))
|
|
54
|
+
return { kind: 'reply', value: result.structuredContent };
|
|
55
|
+
const text = firstTextContent(result.content);
|
|
56
|
+
const parsed = text === null ? null : parsedJson(text);
|
|
57
|
+
return parsed?.ok ? { kind: 'reply', value: parsed.value } : { kind: 'failed', failure: 'unreadable' };
|
|
58
|
+
};
|
|
59
|
+
const requestSignal = (signal, timeoutMs) => {
|
|
60
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
61
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
62
|
+
};
|
|
63
|
+
const httpFailureOf = (status) => {
|
|
64
|
+
if (UNAUTHORIZED_STATUSES.has(status))
|
|
65
|
+
return 'unauthorized';
|
|
66
|
+
return status === TOO_MANY_REQUESTS_STATUS || status >= SERVER_ERROR_STATUS_FLOOR ? 'server_error' : 'refused';
|
|
67
|
+
};
|
|
68
|
+
export const clampedPollMs = (timeoutMs) => Math.min(Math.max(Math.floor(timeoutMs), POLL_TIMEOUT_MIN_MS), POLL_TIMEOUT_MAX_MS);
|
|
69
|
+
export const createPusharyQuestions = (connection, fetcher = fetch) => {
|
|
70
|
+
const endpoint = `${connection.baseUrl}${MCP_PATH}`;
|
|
71
|
+
const callTool = async (name, args, timeoutMs, signal) => {
|
|
72
|
+
const combined = requestSignal(signal, timeoutMs);
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetcher(endpoint, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
Accept: `application/json, ${EVENT_STREAM_TYPE}`,
|
|
79
|
+
Authorization: `Bearer ${connection.apiKey}`,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({ jsonrpc: JSON_RPC_VERSION, id: 1, method: TOOLS_CALL_METHOD, params: { name, arguments: args } }),
|
|
82
|
+
signal: combined,
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
return { kind: 'failed', failure: httpFailureOf(response.status) };
|
|
86
|
+
const body = await response.text();
|
|
87
|
+
return toolReplyOf(envelopeOf(body, response.headers.get('content-type') ?? ''));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
if (signal?.aborted)
|
|
91
|
+
return { kind: 'failed', failure: 'aborted' };
|
|
92
|
+
return { kind: 'failed', failure: combined.aborted ? 'timed_out' : 'network' };
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
ask: args => callTool(ASK_USER_TOOL, { ...args }, ASK_REQUEST_TIMEOUT_MS, undefined),
|
|
97
|
+
waitForAnswer: (correlationId, timeoutMs, signal) => {
|
|
98
|
+
const pollMs = clampedPollMs(timeoutMs);
|
|
99
|
+
return callTool(WAIT_FOR_ANSWER_TOOL, { correlationId, timeoutMs: pollMs }, pollMs + POLL_RESPONSE_GRACE_MS, signal);
|
|
100
|
+
},
|
|
101
|
+
cancel: correlationId => callTool(CANCEL_QUESTION_TOOL, { correlationId }, CANCEL_REQUEST_TIMEOUT_MS, undefined),
|
|
102
|
+
};
|
|
103
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface RedactionRule {
|
|
2
|
+
readonly pattern: RegExp;
|
|
3
|
+
readonly replacement: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const SECRET_REDACTION_RULES: readonly RedactionRule[];
|
|
6
|
+
export declare const redactSecrets: (text: string) => string;
|
|
7
|
+
export declare const redactSecretsFromContent: (text: string) => string;
|
|
8
|
+
export declare const redactSecretsDeep: (text: string) => string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const CREDENTIAL_NAME = String.raw `(?:authorization|secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)["']?`;
|
|
2
|
+
const CREDENTIAL_KEY = String.raw `${CREDENTIAL_NAME}\s*[=:]\s*`;
|
|
3
|
+
const DISPLAYED_CREDENTIAL_KEY = String.raw `${CREDENTIAL_NAME}(?:[ \t]*:[ \t]*|[ \t]+=[ \t]*|=)`;
|
|
4
|
+
const SHELL_INERT_UNIT = String.raw `(?:[A-Za-z0-9_\-./+=:@%^!#]|\\[^\s'"])`;
|
|
5
|
+
const SHELL_INERT_SINGLE_QUOTED_UNIT = String.raw `[A-Za-z0-9_\-./+=:@%^!#\\]`;
|
|
6
|
+
const SHELL_INERT_URL_PASSWORD_UNIT = String.raw `(?:[A-Za-z0-9_\-.+=:%^!#]|\\[^\s'"@/])`;
|
|
7
|
+
const AUTHORIZATION_SCHEME = String.raw `(?:basic|digest|token|negotiate|ntlm)`;
|
|
8
|
+
const PEM_LINE_BREAK = String.raw `(?:\r?\n|(?:\\r)?\\n)`;
|
|
9
|
+
const credentialAssignmentRule = (key, doubleQuoted, singleQuoted, bare) => ({
|
|
10
|
+
pattern: new RegExp(String.raw `(${key})(?:(")${doubleQuoted}|(')${singleQuoted}|(["']?)${bare})`, 'gi'),
|
|
11
|
+
replacement: '$1$2$3$4[redacted]$2$3',
|
|
12
|
+
});
|
|
13
|
+
const DISPLAYED_SECRET_SHAPES = {
|
|
14
|
+
separator: String.raw `[ \t]`,
|
|
15
|
+
privateKeyBody: String.raw `${PEM_LINE_BREAK}?(?:(?:Proc-Type|DEK-Info): [A-Za-z0-9,-]+${PEM_LINE_BREAK})*(?:[A-Za-z0-9+/=\r\n]|\\[rn])*?`,
|
|
16
|
+
urlPassword: `${SHELL_INERT_URL_PASSWORD_UNIT}+`,
|
|
17
|
+
authorizationValue: `${SHELL_INERT_UNIT}+`,
|
|
18
|
+
credentialAssignment: credentialAssignmentRule(DISPLAYED_CREDENTIAL_KEY, `${SHELL_INERT_UNIT}*"`, `${SHELL_INERT_SINGLE_QUOTED_UNIT}*'`, `${SHELL_INERT_UNIT}+`),
|
|
19
|
+
};
|
|
20
|
+
const CUT_OFF_SECRET_SHAPES = {
|
|
21
|
+
separator: String.raw `\s`,
|
|
22
|
+
privateKeyBody: String.raw `[\s\S]*?`,
|
|
23
|
+
urlPassword: String.raw `[^\s@/]+`,
|
|
24
|
+
authorizationValue: String.raw `\S+`,
|
|
25
|
+
credentialAssignment: credentialAssignmentRule(CREDENTIAL_KEY, String.raw `(?:\\[\s\S]|[^"\\])*(?:"|\\?$)`, "[^']*(?:'|$)", String.raw `\S+`),
|
|
26
|
+
};
|
|
27
|
+
const secretRedactionRules = (shapes) => [
|
|
28
|
+
{ pattern: /\bpk_[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g, replacement: '[redacted]' },
|
|
29
|
+
{ pattern: new RegExp(String.raw `-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----${shapes.privateKeyBody}-----END[A-Z0-9 ]*PRIVATE KEY-----`, 'g'), replacement: '[redacted key]' },
|
|
30
|
+
{ pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: '[redacted]' },
|
|
31
|
+
{ pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: '[redacted]' },
|
|
32
|
+
{ pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: '[redacted]' },
|
|
33
|
+
{ pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: '[redacted]' },
|
|
34
|
+
{ pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: '[redacted]' },
|
|
35
|
+
{ pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: '[redacted]' },
|
|
36
|
+
{ pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: '[redacted]' },
|
|
37
|
+
{ pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: '[redacted]' },
|
|
38
|
+
{ pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: '[redacted]' },
|
|
39
|
+
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[redacted]' },
|
|
40
|
+
{ pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: '[redacted]' },
|
|
41
|
+
{ pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: '[redacted]' },
|
|
42
|
+
{ pattern: new RegExp(String.raw `\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)${shapes.urlPassword}@`, 'gi'), replacement: '$1[redacted]@' },
|
|
43
|
+
{ pattern: new RegExp(String.raw `\bbearer${shapes.separator}+[A-Za-z0-9._~+/=-]+`, 'gi'), replacement: 'bearer [redacted]' },
|
|
44
|
+
{ pattern: new RegExp(String.raw `\bauthorization:${shapes.separator}*(?:${AUTHORIZATION_SCHEME}${shapes.separator}+)?${shapes.authorizationValue}`, 'gi'), replacement: 'authorization: [redacted]' },
|
|
45
|
+
shapes.credentialAssignment,
|
|
46
|
+
];
|
|
47
|
+
export const SECRET_REDACTION_RULES = secretRedactionRules(DISPLAYED_SECRET_SHAPES);
|
|
48
|
+
const CUT_OFF_TEXT_REDACTION_RULES = secretRedactionRules(CUT_OFF_SECRET_SHAPES);
|
|
49
|
+
const HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: '[redacted]' };
|
|
50
|
+
const applyRedactionRules = (rules, text) => rules.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
|
|
51
|
+
export const redactSecrets = (text) => applyRedactionRules(SECRET_REDACTION_RULES, text);
|
|
52
|
+
export const redactSecretsFromContent = (text) => applyRedactionRules(CUT_OFF_TEXT_REDACTION_RULES, text);
|
|
53
|
+
export const redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const CANCEL_THEN_ASK_IN_CURRENT_CLIENT = "cancel_then_ask_in_current_client";
|
|
2
|
+
export declare const PUSH_FIRST_DELIVERY = "push_first";
|
|
3
|
+
export type HandBackReason = 'no_devices' | 'current_client';
|
|
4
|
+
export type QuestionState = {
|
|
5
|
+
readonly kind: 'answered';
|
|
6
|
+
readonly value: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'stopped';
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'ended';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'hand_back';
|
|
13
|
+
readonly reason: HandBackReason;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'pending';
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'unreadable';
|
|
18
|
+
};
|
|
19
|
+
export type AskReply = {
|
|
20
|
+
readonly kind: 'question';
|
|
21
|
+
readonly correlationId: string;
|
|
22
|
+
readonly state: QuestionState;
|
|
23
|
+
readonly handBackAfterMs: number | null;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'stopped';
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'unreadable';
|
|
28
|
+
};
|
|
29
|
+
export type Withdrawal = 'withdrawn' | 'not_pending' | 'stop' | 'unreachable';
|
|
30
|
+
export declare const questionStateOf: (reply: unknown) => QuestionState;
|
|
31
|
+
export declare const askReplyOf: (reply: unknown) => AskReply;
|
|
32
|
+
export declare const withdrawalOf: (reply: unknown) => Withdrawal;
|
|
33
|
+
export declare const isExplicitApproval: (answer: string) => boolean;
|
package/dist/replies.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isApprovingAnswer, isDecisionStoppedReply, STOP_HANDOFF_ACTION } from './decision-reply.js';
|
|
2
|
+
export const CANCEL_THEN_ASK_IN_CURRENT_CLIENT = 'cancel_then_ask_in_current_client';
|
|
3
|
+
export const PUSH_FIRST_DELIVERY = 'push_first';
|
|
4
|
+
const PENDING_STATUS = 'pending';
|
|
5
|
+
const UNAVAILABLE_STATUS = 'unavailable';
|
|
6
|
+
const ENDED_STATUSES = new Set(['expired']);
|
|
7
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
const answeredValueOf = (reply) => reply.answered === true && typeof reply.value === 'string' ? reply.value : null;
|
|
9
|
+
const statusOf = (reply) => (typeof reply.status === 'string' ? reply.status : null);
|
|
10
|
+
const hasEnded = (reply) => {
|
|
11
|
+
const status = statusOf(reply);
|
|
12
|
+
return status !== null && ENDED_STATUSES.has(status);
|
|
13
|
+
};
|
|
14
|
+
const handBackReasonOf = (reply) => reply.noDevices === true ? 'no_devices' : 'current_client';
|
|
15
|
+
export const questionStateOf = (reply) => {
|
|
16
|
+
if (!isRecord(reply))
|
|
17
|
+
return { kind: 'unreadable' };
|
|
18
|
+
if (isDecisionStoppedReply(reply))
|
|
19
|
+
return { kind: 'stopped' };
|
|
20
|
+
const answered = answeredValueOf(reply);
|
|
21
|
+
if (answered !== null)
|
|
22
|
+
return { kind: 'answered', value: answered };
|
|
23
|
+
if (reply.answered === true)
|
|
24
|
+
return { kind: 'unreadable' };
|
|
25
|
+
if (hasEnded(reply))
|
|
26
|
+
return { kind: 'ended' };
|
|
27
|
+
if (reply.handoffAction === CANCEL_THEN_ASK_IN_CURRENT_CLIENT)
|
|
28
|
+
return { kind: 'hand_back', reason: handBackReasonOf(reply) };
|
|
29
|
+
if (statusOf(reply) === PENDING_STATUS)
|
|
30
|
+
return { kind: 'pending' };
|
|
31
|
+
return { kind: 'unreadable' };
|
|
32
|
+
};
|
|
33
|
+
const correlationIdOf = (reply) => typeof reply.correlationId === 'string' && reply.correlationId.trim().length > 0 ? reply.correlationId : null;
|
|
34
|
+
const handBackAfterMsOf = (reply) => {
|
|
35
|
+
if (reply.requesterMayAnswer === false || reply.deliveryMode !== PUSH_FIRST_DELIVERY)
|
|
36
|
+
return null;
|
|
37
|
+
const window = reply.policyTimeoutMs;
|
|
38
|
+
return typeof window === 'number' && Number.isFinite(window) && window >= 0 ? window : null;
|
|
39
|
+
};
|
|
40
|
+
export const askReplyOf = (reply) => {
|
|
41
|
+
if (!isRecord(reply))
|
|
42
|
+
return { kind: 'unreadable' };
|
|
43
|
+
const state = questionStateOf(reply);
|
|
44
|
+
if (state.kind === 'stopped')
|
|
45
|
+
return { kind: 'stopped' };
|
|
46
|
+
const correlationId = correlationIdOf(reply);
|
|
47
|
+
if (correlationId === null)
|
|
48
|
+
return { kind: 'unreadable' };
|
|
49
|
+
return { kind: 'question', correlationId, state, handBackAfterMs: handBackAfterMsOf(reply) };
|
|
50
|
+
};
|
|
51
|
+
export const withdrawalOf = (reply) => {
|
|
52
|
+
if (!isRecord(reply))
|
|
53
|
+
return 'unreachable';
|
|
54
|
+
if (statusOf(reply) === UNAVAILABLE_STATUS || reply.handoffAction === STOP_HANDOFF_ACTION)
|
|
55
|
+
return 'stop';
|
|
56
|
+
if (typeof reply.cancelled !== 'boolean')
|
|
57
|
+
return 'unreachable';
|
|
58
|
+
const byThisCall = reply.cancelledByThisCall;
|
|
59
|
+
if (byThisCall !== undefined && typeof byThisCall !== 'boolean')
|
|
60
|
+
return 'unreachable';
|
|
61
|
+
if (byThisCall === true && !reply.cancelled)
|
|
62
|
+
return 'unreachable';
|
|
63
|
+
return reply.cancelled ? 'withdrawn' : 'not_pending';
|
|
64
|
+
};
|
|
65
|
+
export const isExplicitApproval = (answer) => isApprovingAnswer(answer);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { OpenClawBeforeToolCallEvent, OpenClawToolMatcher } from './host-api.js';
|
|
2
|
+
export declare const PLUGIN_ID = "pushary";
|
|
3
|
+
export declare const PLUGIN_NAME = "Pushary";
|
|
4
|
+
export declare const PLUGIN_DESCRIPTION = "Asks you on your phone before OpenClaw runs a gated tool call, and blocks the call unless you approve it.";
|
|
5
|
+
export declare const AGENT_LABEL = "OpenClaw";
|
|
6
|
+
export declare const DEFAULT_OPENCLAW_AGENT_ID = "main";
|
|
7
|
+
export declare const EXEC_TOOL = "exec";
|
|
8
|
+
export declare const PROCESS_TOOL = "process";
|
|
9
|
+
export declare const PLUGINS_TOOL = "plugins";
|
|
10
|
+
export declare const GATEWAY_TOOL = "gateway";
|
|
11
|
+
export declare const OPENCLAW_SYSTEM_TOOL = "openclaw";
|
|
12
|
+
export declare const AUTOMATIONS_TOOL = "automations";
|
|
13
|
+
export declare const TERMINAL_TOOL = "terminal";
|
|
14
|
+
export declare const TOOL_ACTION_PARAMETER = "action";
|
|
15
|
+
export declare const DEFAULT_GATED_TOOLS: readonly string[];
|
|
16
|
+
export interface GateSettings {
|
|
17
|
+
readonly gatedTools: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export type SettingsReading = {
|
|
20
|
+
readonly kind: 'defaulted';
|
|
21
|
+
readonly settings: GateSettings;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'configured';
|
|
24
|
+
readonly settings: GateSettings;
|
|
25
|
+
readonly rejected: readonly string[];
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'invalid';
|
|
28
|
+
readonly settings: GateSettings;
|
|
29
|
+
};
|
|
30
|
+
export declare const hostToolId: (name: string) => string;
|
|
31
|
+
export declare const readGateSettings: (pluginConfig: unknown) => SettingsReading;
|
|
32
|
+
export declare const hookMatcherFor: (settings: GateSettings) => OpenClawToolMatcher;
|
|
33
|
+
export declare const needsApproval: (settings: GateSettings, event: OpenClawBeforeToolCallEvent) => boolean;
|
|
34
|
+
export declare const askingAgentName: (openClawAgentId: string | undefined) => string;
|