@pushary/openclaw 0.0.0-stage → 0.1.0
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/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 +73 -4
package/dist/gate.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { CANCEL_REQUEST_TIMEOUT_MS, POLL_RESPONSE_GRACE_MS, } from './pushary-client.js';
|
|
2
|
+
import { askReplyOf, isExplicitApproval, questionStateOf, withdrawalOf } from './replies.js';
|
|
3
|
+
export const ASK_ATTEMPTS = 3;
|
|
4
|
+
export const ASK_RETRY_PAUSE_MS = 1_000;
|
|
5
|
+
export const POLL_RETRY_PAUSES_MS = [2_000, 4_000, 8_000, 15_000];
|
|
6
|
+
export const UNANSWERED_POLL_PAUSE_MS = 2_000;
|
|
7
|
+
export const LATE_ANSWER_POLL_MS = 1_000;
|
|
8
|
+
export const WITHDRAW_ATTEMPTS = 3;
|
|
9
|
+
export const WITHDRAW_RETRY_PAUSE_MS = 500;
|
|
10
|
+
const RETRYABLE_FAILURES = new Set(['network', 'timed_out', 'server_error']);
|
|
11
|
+
const LATE_ANSWER_REQUEST_MS = LATE_ANSWER_POLL_MS + POLL_RESPONSE_GRACE_MS;
|
|
12
|
+
const decisionFor = (answer) => isExplicitApproval(answer) ? { kind: 'approved' } : { kind: 'denied' };
|
|
13
|
+
const askArgumentsFor = (request, waitEndsAt) => ({
|
|
14
|
+
question: request.question.question,
|
|
15
|
+
type: 'confirm',
|
|
16
|
+
wait: false,
|
|
17
|
+
context: request.question.context,
|
|
18
|
+
agentName: request.agentName,
|
|
19
|
+
toolName: request.question.toolName,
|
|
20
|
+
action: request.question.action,
|
|
21
|
+
actionBody: request.question.actionBody,
|
|
22
|
+
waitEndsAt,
|
|
23
|
+
localSurfaceAvailable: false,
|
|
24
|
+
requestId: request.requestId,
|
|
25
|
+
...(request.question.toolTarget === undefined ? {} : { toolTarget: request.question.toolTarget }),
|
|
26
|
+
...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
|
|
27
|
+
...(request.machineId === undefined ? {} : { machineId: request.machineId }),
|
|
28
|
+
});
|
|
29
|
+
const isRetryable = (failure) => RETRYABLE_FAILURES.has(failure);
|
|
30
|
+
const pollFailureOf = (polled, state) => {
|
|
31
|
+
if (polled.kind === 'failed')
|
|
32
|
+
return polled.failure;
|
|
33
|
+
return state?.kind === 'unreadable' ? 'unreadable_reply' : null;
|
|
34
|
+
};
|
|
35
|
+
const retryPauseMs = (consecutiveFailures) => POLL_RETRY_PAUSES_MS[Math.min(consecutiveFailures, POLL_RETRY_PAUSES_MS.length) - 1] ?? POLL_RETRY_PAUSES_MS[0];
|
|
36
|
+
const pauseAfterPoll = (consecutiveFailures) => consecutiveFailures > 0 ? retryPauseMs(consecutiveFailures) : UNANSWERED_POLL_PAUSE_MS;
|
|
37
|
+
const isAbortFailure = (result) => result.kind === 'failed' && result.failure === 'aborted';
|
|
38
|
+
const isSettled = (state) => state !== null && state.kind !== 'pending' && state.kind !== 'unreadable';
|
|
39
|
+
const withdrawalOfResult = (result) => (result.kind === 'reply' ? withdrawalOf(result.value) : 'unreachable');
|
|
40
|
+
const WITHDRAW_RETRY_NEEDS_MS = WITHDRAW_RETRY_PAUSE_MS + CANCEL_REQUEST_TIMEOUT_MS;
|
|
41
|
+
const mayRetryWithdrawal = (result, attempt, remainingMs) => result.kind === 'failed' && isRetryable(result.failure) && attempt < WITHDRAW_ATTEMPTS && remainingMs >= WITHDRAW_RETRY_NEEDS_MS;
|
|
42
|
+
export const runApprovalGate = async (request, dependencies) => {
|
|
43
|
+
const { questions, clock } = dependencies;
|
|
44
|
+
const startedAt = clock.monotonicMs();
|
|
45
|
+
const deadline = startedAt + request.holdMs;
|
|
46
|
+
const hookDeadline = deadline + request.withdrawReserveMs;
|
|
47
|
+
const aborted = () => request.signal?.aborted === true;
|
|
48
|
+
const remainingMs = (until) => until - clock.monotonicMs();
|
|
49
|
+
const heldMs = () => Math.max(0, clock.monotonicMs() - startedAt);
|
|
50
|
+
const cancelWithRetry = async (correlationId, endsAt) => {
|
|
51
|
+
for (let attempt = 1;; attempt += 1) {
|
|
52
|
+
const result = await questions.cancel(correlationId);
|
|
53
|
+
const withdrawal = withdrawalOfResult(result);
|
|
54
|
+
if (withdrawal !== 'unreachable' || !mayRetryWithdrawal(result, attempt, remainingMs(endsAt)))
|
|
55
|
+
return withdrawal;
|
|
56
|
+
await clock.sleep(WITHDRAW_RETRY_PAUSE_MS, undefined);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const lateAnswerOf = async (correlationId, endsAt) => {
|
|
60
|
+
if (remainingMs(endsAt) < LATE_ANSWER_REQUEST_MS)
|
|
61
|
+
return { kind: 'gone' };
|
|
62
|
+
const late = await questions.waitForAnswer(correlationId, LATE_ANSWER_POLL_MS, undefined);
|
|
63
|
+
const state = late.kind === 'reply' ? questionStateOf(late.value) : null;
|
|
64
|
+
if (state?.kind === 'stopped')
|
|
65
|
+
return { kind: 'stop' };
|
|
66
|
+
return state?.kind === 'answered' ? { kind: 'answered', value: state.value } : { kind: 'gone' };
|
|
67
|
+
};
|
|
68
|
+
const withdraw = async (correlationId) => {
|
|
69
|
+
const endsAt = Math.min(hookDeadline, clock.monotonicMs() + request.withdrawReserveMs);
|
|
70
|
+
const withdrawal = await cancelWithRetry(correlationId, endsAt);
|
|
71
|
+
if (withdrawal === 'not_pending')
|
|
72
|
+
return lateAnswerOf(correlationId, endsAt);
|
|
73
|
+
return { kind: withdrawal };
|
|
74
|
+
};
|
|
75
|
+
const cancelledAfterWithdrawing = async (correlationId) => {
|
|
76
|
+
await withdraw(correlationId);
|
|
77
|
+
return { kind: 'cancelled' };
|
|
78
|
+
};
|
|
79
|
+
const unavailableAfterWithdrawing = async (correlationId, failure) => {
|
|
80
|
+
await withdraw(correlationId);
|
|
81
|
+
return { kind: 'unavailable', failure };
|
|
82
|
+
};
|
|
83
|
+
const handBack = async (correlationId, reason) => {
|
|
84
|
+
const withdrawn = await withdraw(correlationId);
|
|
85
|
+
if (withdrawn.kind === 'answered')
|
|
86
|
+
return decisionFor(withdrawn.value);
|
|
87
|
+
if (withdrawn.kind === 'stop')
|
|
88
|
+
return { kind: 'stopped' };
|
|
89
|
+
if (withdrawn.kind === 'unreachable')
|
|
90
|
+
return { kind: 'unavailable', failure: 'network' };
|
|
91
|
+
return reason === 'no_devices' ? { kind: 'undeliverable' } : { kind: 'handed_back' };
|
|
92
|
+
};
|
|
93
|
+
const createQuestion = async () => {
|
|
94
|
+
const args = askArgumentsFor(request, new Date(clock.wallMs() + request.holdMs).toISOString());
|
|
95
|
+
let last = { kind: 'failed', failure: 'network' };
|
|
96
|
+
for (let attempt = 0; attempt < ASK_ATTEMPTS && remainingMs(deadline) > 0; attempt += 1) {
|
|
97
|
+
if (attempt > 0) {
|
|
98
|
+
if (aborted())
|
|
99
|
+
return last;
|
|
100
|
+
await clock.sleep(Math.min(ASK_RETRY_PAUSE_MS, remainingMs(deadline)), request.signal);
|
|
101
|
+
}
|
|
102
|
+
last = await questions.ask(args);
|
|
103
|
+
if (last.kind === 'reply' || !isRetryable(last.failure))
|
|
104
|
+
return last;
|
|
105
|
+
}
|
|
106
|
+
return last;
|
|
107
|
+
};
|
|
108
|
+
const settleAfterHold = async (correlationId, handBackAt, lastFailure) => {
|
|
109
|
+
const handBackDue = handBackAt !== null && handBackAt < deadline && clock.monotonicMs() >= handBackAt;
|
|
110
|
+
const withdrawn = await withdraw(correlationId);
|
|
111
|
+
if (withdrawn.kind === 'answered')
|
|
112
|
+
return decisionFor(withdrawn.value);
|
|
113
|
+
if (withdrawn.kind === 'stop')
|
|
114
|
+
return { kind: 'stopped' };
|
|
115
|
+
if (withdrawn.kind === 'unreachable')
|
|
116
|
+
return { kind: 'unavailable', failure: lastFailure ?? 'network' };
|
|
117
|
+
if (handBackDue)
|
|
118
|
+
return { kind: 'handed_back' };
|
|
119
|
+
if (lastFailure !== null)
|
|
120
|
+
return { kind: 'unavailable', failure: lastFailure };
|
|
121
|
+
return { kind: 'unanswered', heldMs: heldMs() };
|
|
122
|
+
};
|
|
123
|
+
const settleState = async (correlationId, state) => {
|
|
124
|
+
if (state.kind === 'answered')
|
|
125
|
+
return decisionFor(state.value);
|
|
126
|
+
if (state.kind === 'stopped')
|
|
127
|
+
return { kind: 'stopped' };
|
|
128
|
+
if (state.kind === 'ended')
|
|
129
|
+
return { kind: 'unanswered', heldMs: heldMs() };
|
|
130
|
+
return handBack(correlationId, state.reason);
|
|
131
|
+
};
|
|
132
|
+
const waitForDecision = async (correlationId, handBackAfterMs) => {
|
|
133
|
+
const handBackAt = handBackAfterMs === null ? null : startedAt + handBackAfterMs;
|
|
134
|
+
const until = handBackAt === null ? deadline : Math.min(deadline, handBackAt);
|
|
135
|
+
let lastFailure = null;
|
|
136
|
+
let consecutiveFailures = 0;
|
|
137
|
+
while (remainingMs(until) > 0) {
|
|
138
|
+
if (aborted())
|
|
139
|
+
return cancelledAfterWithdrawing(correlationId);
|
|
140
|
+
const polled = await questions.waitForAnswer(correlationId, remainingMs(until), request.signal);
|
|
141
|
+
if (aborted() || isAbortFailure(polled))
|
|
142
|
+
return cancelledAfterWithdrawing(correlationId);
|
|
143
|
+
const state = polled.kind === 'reply' ? questionStateOf(polled.value) : null;
|
|
144
|
+
if (isSettled(state))
|
|
145
|
+
return settleState(correlationId, state);
|
|
146
|
+
lastFailure = pollFailureOf(polled, state);
|
|
147
|
+
if (lastFailure !== null && !isRetryable(lastFailure))
|
|
148
|
+
return unavailableAfterWithdrawing(correlationId, lastFailure);
|
|
149
|
+
consecutiveFailures = lastFailure === null ? 0 : consecutiveFailures + 1;
|
|
150
|
+
await clock.sleep(Math.min(pauseAfterPoll(consecutiveFailures), Math.max(0, remainingMs(until))), request.signal);
|
|
151
|
+
}
|
|
152
|
+
if (aborted())
|
|
153
|
+
return cancelledAfterWithdrawing(correlationId);
|
|
154
|
+
return settleAfterHold(correlationId, handBackAt, lastFailure);
|
|
155
|
+
};
|
|
156
|
+
if (aborted())
|
|
157
|
+
return { kind: 'cancelled' };
|
|
158
|
+
const created = await createQuestion();
|
|
159
|
+
const reply = created.kind === 'reply' ? askReplyOf(created.value) : null;
|
|
160
|
+
if (aborted())
|
|
161
|
+
return reply?.kind === 'question' ? cancelledAfterWithdrawing(reply.correlationId) : { kind: 'cancelled' };
|
|
162
|
+
if (created.kind === 'failed')
|
|
163
|
+
return { kind: 'unavailable', failure: created.failure };
|
|
164
|
+
if (reply === null || reply.kind === 'unreadable')
|
|
165
|
+
return { kind: 'unavailable', failure: 'unreadable_reply' };
|
|
166
|
+
if (reply.kind === 'stopped')
|
|
167
|
+
return { kind: 'stopped' };
|
|
168
|
+
if (reply.state.kind === 'pending')
|
|
169
|
+
return waitForDecision(reply.correlationId, reply.handBackAfterMs);
|
|
170
|
+
if (reply.state.kind === 'unreadable')
|
|
171
|
+
return unavailableAfterWithdrawing(reply.correlationId, 'unreadable_reply');
|
|
172
|
+
return settleState(reply.correlationId, reply.state);
|
|
173
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const REQUESTED_HOOK_TIMEOUT_MS = 300000;
|
|
2
|
+
export declare const WITHDRAW_RESERVE_MS = 10000;
|
|
3
|
+
export declare const OPERATOR_HOOK_TIMEOUT_MAX_MS = 600000;
|
|
4
|
+
export type HookBudgetSource = 'plugin' | 'operator_hook_timeout' | 'operator_plugin_timeout';
|
|
5
|
+
export interface HookBudget {
|
|
6
|
+
readonly timeoutMs: number;
|
|
7
|
+
readonly source: HookBudgetSource;
|
|
8
|
+
}
|
|
9
|
+
export declare const hookBudgetFrom: (hostConfig: unknown, pluginId: string) => HookBudget;
|
|
10
|
+
export declare const holdMsWithin: (hookTimeoutMs: number) => number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BEFORE_TOOL_CALL_HOOK } from './host-api.js';
|
|
2
|
+
export const REQUESTED_HOOK_TIMEOUT_MS = 300_000;
|
|
3
|
+
export const WITHDRAW_RESERVE_MS = 10_000;
|
|
4
|
+
export const OPERATOR_HOOK_TIMEOUT_MAX_MS = 600_000;
|
|
5
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
const fieldOf = (value, key) => (isRecord(value) ? value[key] : undefined);
|
|
7
|
+
const operatorMilliseconds = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 && value <= OPERATOR_HOOK_TIMEOUT_MAX_MS ? value : null;
|
|
8
|
+
const hookPolicyOf = (hostConfig, pluginId) => fieldOf(fieldOf(fieldOf(fieldOf(hostConfig, 'plugins'), 'entries'), pluginId), 'hooks');
|
|
9
|
+
export const hookBudgetFrom = (hostConfig, pluginId) => {
|
|
10
|
+
const policy = hookPolicyOf(hostConfig, pluginId);
|
|
11
|
+
const perHook = operatorMilliseconds(fieldOf(fieldOf(policy, 'timeouts'), BEFORE_TOOL_CALL_HOOK));
|
|
12
|
+
if (perHook !== null)
|
|
13
|
+
return { timeoutMs: perHook, source: 'operator_hook_timeout' };
|
|
14
|
+
const perPlugin = operatorMilliseconds(fieldOf(policy, 'timeoutMs'));
|
|
15
|
+
if (perPlugin !== null)
|
|
16
|
+
return { timeoutMs: perPlugin, source: 'operator_plugin_timeout' };
|
|
17
|
+
return { timeoutMs: REQUESTED_HOOK_TIMEOUT_MS, source: 'plugin' };
|
|
18
|
+
};
|
|
19
|
+
export const holdMsWithin = (hookTimeoutMs) => Math.max(hookTimeoutMs - WITHDRAW_RESERVE_MS, Math.floor(hookTimeoutMs / 2));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export declare const BEFORE_TOOL_CALL_HOOK = "before_tool_call";
|
|
2
|
+
export type OpenClawApprovalDecision = 'allow-once' | 'allow-always' | 'deny';
|
|
3
|
+
export type OpenClawApprovalSeverity = 'info' | 'warning' | 'critical';
|
|
4
|
+
export type OpenClawToolMatcher = readonly [string, ...string[]];
|
|
5
|
+
export interface OpenClawBeforeToolCallEvent {
|
|
6
|
+
readonly toolName: string;
|
|
7
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
8
|
+
readonly toolKind?: string;
|
|
9
|
+
readonly toolInputKind?: string;
|
|
10
|
+
readonly runId?: string;
|
|
11
|
+
readonly toolCallId?: string;
|
|
12
|
+
readonly derivedPaths?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface OpenClawToolContext {
|
|
15
|
+
readonly agentId?: string;
|
|
16
|
+
readonly sessionKey?: string;
|
|
17
|
+
readonly sessionId?: string;
|
|
18
|
+
readonly runId?: string;
|
|
19
|
+
readonly toolCallId?: string;
|
|
20
|
+
readonly channelId?: string;
|
|
21
|
+
readonly abortSignal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
export interface OpenClawApprovalRequest {
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
severity?: OpenClawApprovalSeverity;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
timeoutReason?: string;
|
|
29
|
+
allowedDecisions?: OpenClawApprovalDecision[];
|
|
30
|
+
}
|
|
31
|
+
export interface OpenClawBeforeToolCallResult {
|
|
32
|
+
block?: boolean;
|
|
33
|
+
blockReason?: string;
|
|
34
|
+
requireApproval?: OpenClawApprovalRequest;
|
|
35
|
+
}
|
|
36
|
+
export type OpenClawBeforeToolCallHandler = (event: OpenClawBeforeToolCallEvent, context: OpenClawToolContext) => Promise<OpenClawBeforeToolCallResult | undefined>;
|
|
37
|
+
export interface OpenClawHookOptions {
|
|
38
|
+
readonly matcher?: OpenClawToolMatcher;
|
|
39
|
+
readonly priority?: number;
|
|
40
|
+
readonly timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface OpenClawLogger {
|
|
43
|
+
readonly info: (message: string) => void;
|
|
44
|
+
readonly warn: (message: string) => void;
|
|
45
|
+
readonly error: (message: string) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface OpenClawPluginApi {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly config: unknown;
|
|
50
|
+
readonly pluginConfig?: unknown;
|
|
51
|
+
readonly runtime?: unknown;
|
|
52
|
+
readonly logger: OpenClawLogger;
|
|
53
|
+
readonly on: (hookName: typeof BEFORE_TOOL_CALL_HOOK, handler: OpenClawBeforeToolCallHandler, options?: OpenClawHookOptions) => void;
|
|
54
|
+
}
|
|
55
|
+
export interface OpenClawPluginEntry {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly description: string;
|
|
59
|
+
readonly register: (api: OpenClawPluginApi) => void;
|
|
60
|
+
}
|
package/dist/host-api.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BEFORE_TOOL_CALL_HOOK = 'before_tool_call';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const MIN_HOST_VERSION = "2026.11.3";
|
|
2
|
+
export type HostSupport = {
|
|
3
|
+
readonly kind: 'supported';
|
|
4
|
+
readonly version: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: 'too_old';
|
|
7
|
+
readonly version: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: 'unknown';
|
|
10
|
+
};
|
|
11
|
+
export declare const hostSupportFrom: (runtime: unknown) => HostSupport;
|
|
12
|
+
export declare const handsBackToTheHost: (support: HostSupport) => boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const MIN_HOST_VERSION = '2026.11.3';
|
|
2
|
+
const RELEASE_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/;
|
|
3
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
const releaseVersionOf = (text) => {
|
|
5
|
+
const match = RELEASE_VERSION_PATTERN.exec(text.trim());
|
|
6
|
+
if (match === null)
|
|
7
|
+
return null;
|
|
8
|
+
const [, major, minor, patch, prerelease] = match;
|
|
9
|
+
return { parts: [Number(major), Number(minor), Number(patch)], prerelease: prerelease !== undefined };
|
|
10
|
+
};
|
|
11
|
+
const isAtLeast = (version, floor) => {
|
|
12
|
+
const differing = version.parts.findIndex((part, index) => part !== floor.parts[index]);
|
|
13
|
+
if (differing === -1)
|
|
14
|
+
return !version.prerelease || floor.prerelease;
|
|
15
|
+
return (version.parts[differing] ?? 0) > (floor.parts[differing] ?? 0);
|
|
16
|
+
};
|
|
17
|
+
const FLOOR = releaseVersionOf(MIN_HOST_VERSION);
|
|
18
|
+
export const hostSupportFrom = (runtime) => {
|
|
19
|
+
const version = isRecord(runtime) && typeof runtime.version === 'string' ? runtime.version : null;
|
|
20
|
+
const parsed = version === null ? null : releaseVersionOf(version);
|
|
21
|
+
if (version === null || parsed === null || FLOOR === null)
|
|
22
|
+
return { kind: 'unknown' };
|
|
23
|
+
return isAtLeast(parsed, FLOOR) ? { kind: 'supported', version } : { kind: 'too_old', version };
|
|
24
|
+
};
|
|
25
|
+
export const handsBackToTheHost = (support) => support.kind === 'supported';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type GateEnvironment } from './approval-hook.js';
|
|
2
|
+
import type { GateClock } from './gate.js';
|
|
3
|
+
import type { OpenClawPluginEntry } from './host-api.js';
|
|
4
|
+
export declare const systemClock: GateClock;
|
|
5
|
+
export declare const processGateEnvironment: () => GateEnvironment;
|
|
6
|
+
declare const pusharyPlugin: OpenClawPluginEntry;
|
|
7
|
+
export default pusharyPlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { registerApprovalGate } from './approval-hook.js';
|
|
4
|
+
import { processConnectionSources, readPusharyConnection } from './connection.js';
|
|
5
|
+
import { createPusharyQuestions } from './pushary-client.js';
|
|
6
|
+
import { PLUGIN_DESCRIPTION, PLUGIN_ID, PLUGIN_NAME } from './settings.js';
|
|
7
|
+
const sleepUnlessAborted = async (ms, signal) => {
|
|
8
|
+
try {
|
|
9
|
+
await delay(ms, undefined, signal === undefined ? undefined : { signal });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const systemClock = {
|
|
16
|
+
monotonicMs: () => performance.now(),
|
|
17
|
+
wallMs: () => Date.now(),
|
|
18
|
+
sleep: sleepUnlessAborted,
|
|
19
|
+
};
|
|
20
|
+
export const processGateEnvironment = () => ({
|
|
21
|
+
readConnection: () => readPusharyConnection(processConnectionSources()),
|
|
22
|
+
questionsFor: connection => createPusharyQuestions(connection),
|
|
23
|
+
clock: systemClock,
|
|
24
|
+
newRequestId: randomUUID,
|
|
25
|
+
});
|
|
26
|
+
const pusharyPlugin = {
|
|
27
|
+
id: PLUGIN_ID,
|
|
28
|
+
name: PLUGIN_NAME,
|
|
29
|
+
description: PLUGIN_DESCRIPTION,
|
|
30
|
+
register: api => registerApprovalGate(api, processGateEnvironment()),
|
|
31
|
+
};
|
|
32
|
+
export default pusharyPlugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const OPENCLAW_ENVIRONMENT_KEYS: readonly ["OPENCLAW_HOME", "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH", "OPENCLAW_PROFILE"];
|
|
2
|
+
export type OpenClawEnvironmentKey = (typeof OPENCLAW_ENVIRONMENT_KEYS)[number];
|
|
3
|
+
export type OpenClawEnvironment = Readonly<Partial<Record<OpenClawEnvironmentKey, string>>>;
|
|
4
|
+
export interface OpenClawPaths {
|
|
5
|
+
readonly home: string;
|
|
6
|
+
readonly stateDirectory: string;
|
|
7
|
+
readonly configFile: string;
|
|
8
|
+
readonly environmentFile: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const OWNED_PLUGIN_FOLDER_NAME = "openclaw-plugin";
|
|
11
|
+
export declare const openClawPathsFrom: (environment: OpenClawEnvironment, osHome: string) => OpenClawPaths;
|
|
12
|
+
export declare const openClawDirectoriesToLookIn: (paths: OpenClawPaths) => readonly string[];
|
|
13
|
+
export declare const openClawEnvironmentOf: (environment: Readonly<Record<string, string | undefined>>) => OpenClawEnvironment;
|
|
14
|
+
export declare const ownedPluginFolderIn: (osHome: string) => string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Generated by scripts/generate-editor-redaction.mjs.
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
export const OPENCLAW_ENVIRONMENT_KEYS = ['OPENCLAW_HOME', 'OPENCLAW_STATE_DIR', 'OPENCLAW_CONFIG_PATH', 'OPENCLAW_PROFILE'];
|
|
4
|
+
const STATE_DIRECTORY_NAME = '.openclaw';
|
|
5
|
+
const PUSHARY_DIRECTORY_NAME = '.pushary';
|
|
6
|
+
export const OWNED_PLUGIN_FOLDER_NAME = 'openclaw-plugin';
|
|
7
|
+
const CONFIG_FILE_NAME = 'openclaw.json';
|
|
8
|
+
const ENVIRONMENT_FILE_NAME = '.env';
|
|
9
|
+
const UNSET_SPELLINGS = new Set(['undefined', 'null']);
|
|
10
|
+
const HOME_PREFIX = /^~(?=$|[\\/])/;
|
|
11
|
+
const setValue = (value) => {
|
|
12
|
+
const trimmed = value?.trim();
|
|
13
|
+
return trimmed && !UNSET_SPELLINGS.has(trimmed) ? trimmed : undefined;
|
|
14
|
+
};
|
|
15
|
+
const expandedFrom = (path, home) => resolve(path.replace(HOME_PREFIX, () => home));
|
|
16
|
+
const homeOf = (environment, osHome) => {
|
|
17
|
+
const configured = setValue(environment.OPENCLAW_HOME);
|
|
18
|
+
if (configured === undefined)
|
|
19
|
+
return resolve(osHome);
|
|
20
|
+
return HOME_PREFIX.test(configured) ? expandedFrom(configured, osHome) : resolve(configured);
|
|
21
|
+
};
|
|
22
|
+
const pathSetting = (value, home) => {
|
|
23
|
+
const configured = value?.trim();
|
|
24
|
+
if (!configured)
|
|
25
|
+
return undefined;
|
|
26
|
+
return HOME_PREFIX.test(configured) ? expandedFrom(configured, home) : resolve(configured);
|
|
27
|
+
};
|
|
28
|
+
export const openClawPathsFrom = (environment, osHome) => {
|
|
29
|
+
const home = homeOf(environment, osHome);
|
|
30
|
+
const stateDirectory = pathSetting(environment.OPENCLAW_STATE_DIR, home) ?? join(home, STATE_DIRECTORY_NAME);
|
|
31
|
+
return {
|
|
32
|
+
home,
|
|
33
|
+
stateDirectory,
|
|
34
|
+
configFile: pathSetting(environment.OPENCLAW_CONFIG_PATH, home) ?? join(stateDirectory, CONFIG_FILE_NAME),
|
|
35
|
+
environmentFile: join(stateDirectory, ENVIRONMENT_FILE_NAME),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export const openClawDirectoriesToLookIn = (paths) => [...new Set([paths.stateDirectory, paths.configFile, join(paths.home, STATE_DIRECTORY_NAME)])];
|
|
39
|
+
export const openClawEnvironmentOf = (environment) => Object.fromEntries(OPENCLAW_ENVIRONMENT_KEYS.flatMap(key => {
|
|
40
|
+
const value = environment[key];
|
|
41
|
+
return value === undefined ? [] : [[key, value]];
|
|
42
|
+
}));
|
|
43
|
+
export const ownedPluginFolderIn = (osHome) => join(osHome, PUSHARY_DIRECTORY_NAME, OWNED_PLUGIN_FOLDER_NAME);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from './host-api.js';
|
|
2
|
+
import { type FileWriteCall, type OpenClawStateLocations } from './protected-writes.js';
|
|
3
|
+
export declare const stateDirectoryHoldingPlugin: (pluginFile: string) => string | null;
|
|
4
|
+
export declare const openClawStateLocationsFor: (api: OpenClawPluginApi, environment: Readonly<Record<string, string | undefined>>, osHome: string, pluginFile: string, gatewayWorkingDirectory: string) => OpenClawStateLocations;
|
|
5
|
+
export declare const openClawWriteGuardFor: (api: OpenClawPluginApi) => ((call: FileWriteCall) => boolean);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { openClawEnvironmentOf, openClawPathsFrom, ownedPluginFolderIn } from './openclaw-paths.js';
|
|
5
|
+
import { nodeRealpath, spawnsOutsideTheWorkspace, writesIntoOpenClaw } from './protected-writes.js';
|
|
6
|
+
import { DEFAULT_OPENCLAW_AGENT_ID } from './settings.js';
|
|
7
|
+
const CASE_INSENSITIVE_PLATFORMS = new Set(['darwin', 'win32']);
|
|
8
|
+
const EXTENSIONS_DIRECTORY_NAME = 'extensions';
|
|
9
|
+
const PLUGIN_ROOT_DEPTH = 2;
|
|
10
|
+
const USER_CONFIG_DIRECTORY = '.config';
|
|
11
|
+
const GATEWAY_ENV_FALLBACK_DIRECTORY = 'openclaw';
|
|
12
|
+
const LAUNCHD_LABEL_PREFIX = 'ai.openclaw';
|
|
13
|
+
const SYSTEMD_UNIT_PREFIX = 'openclaw';
|
|
14
|
+
const USER_LAUNCH_AGENTS = ['Library', 'LaunchAgents'];
|
|
15
|
+
const USER_SYSTEMD_UNITS = ['systemd', 'user'];
|
|
16
|
+
const SYSTEM_SERVICE_FILES = [
|
|
17
|
+
{ directory: '/Library/LaunchDaemons', namePrefix: LAUNCHD_LABEL_PREFIX },
|
|
18
|
+
{ directory: '/etc/systemd/system', namePrefix: SYSTEMD_UNIT_PREFIX },
|
|
19
|
+
];
|
|
20
|
+
const GATEWAY_WORKING_ENV_FILE = '.env';
|
|
21
|
+
const LAUNCHD_LABEL_VARIABLE = 'OPENCLAW_LAUNCHD_LABEL';
|
|
22
|
+
const SYSTEMD_UNIT_VARIABLE = 'OPENCLAW_SYSTEMD_UNIT';
|
|
23
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
|
+
const hostFunction = (namespace, name) => {
|
|
25
|
+
if (!isRecord(namespace))
|
|
26
|
+
return null;
|
|
27
|
+
const candidate = namespace[name];
|
|
28
|
+
return typeof candidate === 'function' ? (...args) => Reflect.apply(candidate, namespace, args) : null;
|
|
29
|
+
};
|
|
30
|
+
const hostPath = (resolver, ...args) => {
|
|
31
|
+
if (resolver === null)
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
const value = resolver(...args);
|
|
35
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const runtimeNamespace = (api, name) => (isRecord(api.runtime) ? api.runtime[name] : undefined);
|
|
42
|
+
export const stateDirectoryHoldingPlugin = (pluginFile) => {
|
|
43
|
+
let pluginRoot = pluginFile;
|
|
44
|
+
for (let level = 0; level < PLUGIN_ROOT_DEPTH; level += 1)
|
|
45
|
+
pluginRoot = dirname(pluginRoot);
|
|
46
|
+
const extensions = dirname(pluginRoot);
|
|
47
|
+
return basename(extensions) === EXTENSIONS_DIRECTORY_NAME ? dirname(extensions) : null;
|
|
48
|
+
};
|
|
49
|
+
const presentOnly = (paths) => [...new Set(paths.flatMap(path => (path === null ? [] : [path])))];
|
|
50
|
+
export const openClawStateLocationsFor = (api, environment, osHome, pluginFile, gatewayWorkingDirectory) => {
|
|
51
|
+
const active = openClawPathsFrom(openClawEnvironmentOf(environment), osHome);
|
|
52
|
+
const persistent = openClawPathsFrom(openClawEnvironmentOf({ OPENCLAW_HOME: environment.OPENCLAW_HOME }), osHome);
|
|
53
|
+
const hostState = hostPath(hostFunction(runtimeNamespace(api, 'state'), 'resolveStateDir'), environment);
|
|
54
|
+
const homes = presentOnly([osHome, active.home]);
|
|
55
|
+
return {
|
|
56
|
+
stateDirectories: presentOnly([active.stateDirectory, persistent.stateDirectory, hostState, stateDirectoryHoldingPlugin(pluginFile)]),
|
|
57
|
+
configFiles: presentOnly([active.configFile, persistent.configFile, join(gatewayWorkingDirectory, GATEWAY_WORKING_ENV_FILE)]),
|
|
58
|
+
protectedTrees: presentOnly([
|
|
59
|
+
dirname(ownedPluginFolderIn(osHome)),
|
|
60
|
+
...homes.map(home => join(home, USER_CONFIG_DIRECTORY, GATEWAY_ENV_FALLBACK_DIRECTORY)),
|
|
61
|
+
]),
|
|
62
|
+
serviceFiles: serviceFilesFor(homes, environment),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const overriddenServiceNames = (environment) => presentOnly([environment[LAUNCHD_LABEL_VARIABLE]?.trim() || null, environment[SYSTEMD_UNIT_VARIABLE]?.trim() || null]);
|
|
66
|
+
const serviceFilesFor = (homes, environment) => {
|
|
67
|
+
const directories = [
|
|
68
|
+
...homes.map(home => join(home, ...USER_LAUNCH_AGENTS)),
|
|
69
|
+
...homes.map(home => join(home, USER_CONFIG_DIRECTORY, ...USER_SYSTEMD_UNITS)),
|
|
70
|
+
...SYSTEM_SERVICE_FILES.map(service => service.directory),
|
|
71
|
+
];
|
|
72
|
+
return [
|
|
73
|
+
...homes.flatMap(home => [
|
|
74
|
+
{ directory: join(home, ...USER_LAUNCH_AGENTS), namePrefix: LAUNCHD_LABEL_PREFIX },
|
|
75
|
+
{ directory: join(home, USER_CONFIG_DIRECTORY, ...USER_SYSTEMD_UNITS), namePrefix: SYSTEMD_UNIT_PREFIX },
|
|
76
|
+
]),
|
|
77
|
+
...SYSTEM_SERVICE_FILES,
|
|
78
|
+
...overriddenServiceNames(environment).flatMap(name => directories.map(directory => ({ directory, namePrefix: name }))),
|
|
79
|
+
];
|
|
80
|
+
};
|
|
81
|
+
const workspaceFrom = (api) => (agentId) => hostPath(hostFunction(runtimeNamespace(api, 'agent'), 'resolveAgentWorkspaceDir'), api.config, agentId ?? DEFAULT_OPENCLAW_AGENT_ID);
|
|
82
|
+
export const openClawWriteGuardFor = (api) => {
|
|
83
|
+
const osHome = homedir();
|
|
84
|
+
const locations = openClawStateLocationsFor(api, process.env, osHome, fileURLToPath(import.meta.url), process.cwd());
|
|
85
|
+
const system = {
|
|
86
|
+
osHome,
|
|
87
|
+
realpath: nodeRealpath,
|
|
88
|
+
caseInsensitive: CASE_INSENSITIVE_PLATFORMS.has(process.platform),
|
|
89
|
+
workspaceFor: workspaceFrom(api),
|
|
90
|
+
otherWorkingDirectories: [process.cwd()],
|
|
91
|
+
};
|
|
92
|
+
return call => writesIntoOpenClaw(locations, system, call) || spawnsOutsideTheWorkspace(locations, system, call);
|
|
93
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OpenClawBeforeToolCallEvent } from './host-api.js';
|
|
2
|
+
export declare const WRITE_TOOL = "write";
|
|
3
|
+
export declare const EDIT_TOOL = "edit";
|
|
4
|
+
export declare const APPLY_PATCH_TOOL = "apply_patch";
|
|
5
|
+
export declare const FILE_WRITING_TOOLS: readonly string[];
|
|
6
|
+
export interface ServiceFileLocation {
|
|
7
|
+
readonly directory: string;
|
|
8
|
+
readonly namePrefix: string;
|
|
9
|
+
}
|
|
10
|
+
export interface OpenClawStateLocations {
|
|
11
|
+
readonly stateDirectories: readonly string[];
|
|
12
|
+
readonly configFiles: readonly string[];
|
|
13
|
+
readonly protectedTrees: readonly string[];
|
|
14
|
+
readonly serviceFiles: readonly ServiceFileLocation[];
|
|
15
|
+
}
|
|
16
|
+
export interface WriteGuardSystem {
|
|
17
|
+
readonly osHome: string;
|
|
18
|
+
readonly realpath: (path: string) => string;
|
|
19
|
+
readonly caseInsensitive: boolean;
|
|
20
|
+
readonly workspaceFor: (agentId: string | undefined) => string | null;
|
|
21
|
+
readonly otherWorkingDirectories: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export interface FileWriteCall {
|
|
24
|
+
readonly toolId: string;
|
|
25
|
+
readonly event: OpenClawBeforeToolCallEvent;
|
|
26
|
+
readonly agentId: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
export declare const patchTargetsOf: (patch: unknown) => readonly string[];
|
|
29
|
+
export declare const writesIntoOpenClaw: (locations: OpenClawStateLocations, system: WriteGuardSystem, call: FileWriteCall) => boolean;
|
|
30
|
+
export declare const SESSIONS_SPAWN_TOOL = "sessions_spawn";
|
|
31
|
+
export declare const SPAWN_FOLDER_PARAMETER = "cwd";
|
|
32
|
+
export declare const spawnsOutsideTheWorkspace: (locations: OpenClawStateLocations, system: WriteGuardSystem, call: FileWriteCall) => boolean;
|
|
33
|
+
export declare const nodeRealpath: (path: string) => string;
|