@hybridaione/hybridclaw 0.1.19 → 0.1.20
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/.env.example +7 -0
- package/.hybridclaw/container-image-state.json +2 -2
- package/CHANGELOG.md +16 -0
- package/README.md +5 -0
- package/SECURITY.md +8 -0
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/browser-tools.ts +81 -12
- package/docs/index.html +6 -2
- package/package.json +1 -1
- package/src/audit-events.ts +27 -1
- package/src/prompt-hooks.ts +6 -0
package/.env.example
CHANGED
|
@@ -5,3 +5,10 @@ HYBRIDAI_API_KEY=
|
|
|
5
5
|
DISCORD_TOKEN= # Enable Discord integration when set
|
|
6
6
|
WEB_API_TOKEN= # Protect /api/* endpoints (Bearer token)
|
|
7
7
|
GATEWAY_API_TOKEN= # Client token override (defaults to WEB_API_TOKEN)
|
|
8
|
+
|
|
9
|
+
# Optional browser settings
|
|
10
|
+
BROWSER_ALLOW_PRIVATE_NETWORK= # Set true to allow localhost/private network navigation in browser tools
|
|
11
|
+
BROWSER_PERSIST_PROFILE= # Default true; set false for ephemeral browser profile
|
|
12
|
+
BROWSER_PERSIST_SESSION_STATE= # Default true; set false to disable auto save/restore state files
|
|
13
|
+
BROWSER_PROFILE_ROOT= # Optional profile root path (default: /workspace/.hybridclaw-runtime/browser-profiles in container)
|
|
14
|
+
BROWSER_CDP_URL= # Optional CDP endpoint (e.g. ws://127.0.0.1:9222/devtools/browser/...)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"imageName": "hybridclaw-agent",
|
|
3
|
-
"fingerprint": "
|
|
4
|
-
"recordedAt": "2026-03-
|
|
3
|
+
"fingerprint": "3d8807c236f660de18d8e98a9539ca1862a2ed522f1a9acf9de862c5d12c4c75",
|
|
4
|
+
"recordedAt": "2026-03-02T20:30:27.276Z"
|
|
5
5
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
### Fixed
|
|
10
10
|
|
|
11
|
+
## [0.1.20](https://github.com/HybridAIOne/hybridclaw/tree/v0.1.20)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Browser auth policy clarification**: Added explicit runtime guidance that user-directed login/auth-flow testing is allowed with browser tools on the requested domain.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Persistent browser login continuity**: Browser tooling now persists per-session profile/state by default (`AGENT_BROWSER_PROFILE` + `AGENT_BROWSER_SESSION_NAME`) with configurable overrides (`BROWSER_PERSIST_PROFILE`, `BROWSER_PERSIST_SESSION_STATE`, `BROWSER_PROFILE_ROOT`, `BROWSER_CDP_URL`).
|
|
20
|
+
- **Safety prompt alignment**: System safety hook now explicitly rejects fabricated “public-only/unauthenticated browser” limitations and prioritizes real tool/policy outcomes.
|
|
21
|
+
- **Documentation refresh**: Updated README and website docs (`docs/index.html`) with authenticated browser-flow support and browser session persistence behavior.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Audit secret leakage risk**: Structured audit tool-call arguments now redact sensitive fields (password/token/secret/etc.), including `browser_type.text`, to avoid credential plaintext in audit trails.
|
|
26
|
+
|
|
11
27
|
## [0.1.19](https://github.com/HybridAIOne/hybridclaw/tree/v0.1.19)
|
|
12
28
|
|
|
13
29
|
### Added
|
package/README.md
CHANGED
|
@@ -246,6 +246,11 @@ Browser tooling notes:
|
|
|
246
246
|
|
|
247
247
|
- The shipped container image preinstalls `agent-browser` and Chromium (Playwright).
|
|
248
248
|
- You can override the binary via `AGENT_BROWSER_BIN` if needed.
|
|
249
|
+
- User-directed authenticated browser-flow testing is supported (including filling/submitting login forms on the requested site).
|
|
250
|
+
- Browser auth/session state now persists per HybridClaw session by default via a dedicated profile directory under `/workspace/.hybridclaw-runtime/browser-profiles`.
|
|
251
|
+
- Session cookies/localStorage are also auto-saved/restored via `agent-browser` session-state files.
|
|
252
|
+
- Optional overrides: `BROWSER_PERSIST_PROFILE=false` (disable profile persistence), `BROWSER_PERSIST_SESSION_STATE=false` (disable state file persistence), `BROWSER_PROFILE_ROOT=/path` (custom profile root), `BROWSER_CDP_URL=ws://...` (force CDP attachment to an existing browser).
|
|
253
|
+
- Structured audit logs redact sensitive browser/tool arguments (password/token/secret fields and typed form text).
|
|
249
254
|
- Navigation to private/loopback hosts is blocked by default (set `BROWSER_ALLOW_PRIVATE_NETWORK=true` to override).
|
|
250
255
|
- Screenshot/PDF outputs are constrained to `/workspace/.browser-artifacts`.
|
|
251
256
|
|
package/SECURITY.md
CHANGED
|
@@ -22,6 +22,14 @@ System prompts include safety constraints for every conversation turn:
|
|
|
22
22
|
|
|
23
23
|
Implementation: [src/prompt-hooks.ts](./src/prompt-hooks.ts)
|
|
24
24
|
|
|
25
|
+
### 1.1) Browser Authentication Flows
|
|
26
|
+
|
|
27
|
+
User-directed browser authentication testing is permitted when the user explicitly asks for it:
|
|
28
|
+
|
|
29
|
+
- Browser tools may fill credentials and submit login forms for the requested site.
|
|
30
|
+
- Credentials must be used only for the requested auth flow on the intended domain.
|
|
31
|
+
- Credentials must not be echoed in assistant prose, written to workspace files, or sent to unrelated domains.
|
|
32
|
+
|
|
25
33
|
### 2) Runtime Tool Blocking
|
|
26
34
|
|
|
27
35
|
Before tool execution, HybridClaw applies policy hooks that block known dangerous patterns:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hybridclaw-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hybridclaw-agent",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.20",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@mozilla/readability": "^0.6.0",
|
|
12
12
|
"agent-browser": "^0.15.1",
|
package/container/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile, spawnSync } from 'child_process';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
2
3
|
import { lookup } from 'dns/promises';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import net from 'net';
|
|
@@ -19,6 +20,8 @@ const BROWSER_TMP_HOME = path.join(BROWSER_RUNTIME_ROOT, 'home');
|
|
|
19
20
|
const BROWSER_NPM_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'npm-cache');
|
|
20
21
|
const BROWSER_XDG_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'cache');
|
|
21
22
|
const BROWSER_PLAYWRIGHT_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'ms-playwright');
|
|
23
|
+
const BROWSER_PROFILE_ROOT = path.join(BROWSER_RUNTIME_ROOT, 'browser-profiles');
|
|
24
|
+
const ENV_FALSEY = new Set(['0', 'false', 'no', 'off']);
|
|
22
25
|
|
|
23
26
|
type BrowserRunner = {
|
|
24
27
|
cmd: string;
|
|
@@ -28,6 +31,8 @@ type BrowserRunner = {
|
|
|
28
31
|
type BrowserSession = {
|
|
29
32
|
sessionKey: string;
|
|
30
33
|
socketDir: string;
|
|
34
|
+
profileDir?: string;
|
|
35
|
+
stateName?: string;
|
|
31
36
|
createdAt: number;
|
|
32
37
|
lastUsedAt: number;
|
|
33
38
|
};
|
|
@@ -44,6 +49,45 @@ function normalizeSessionKey(sessionId: string): string {
|
|
|
44
49
|
return normalized || 'default';
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function envFlagEnabled(name: string, defaultValue: boolean): boolean {
|
|
53
|
+
const raw = process.env[name];
|
|
54
|
+
if (raw == null || raw.trim() === '') return defaultValue;
|
|
55
|
+
return !ENV_FALSEY.has(raw.trim().toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function deriveStableId(raw: string, maxLength = 40): string {
|
|
59
|
+
const base =
|
|
60
|
+
String(raw || 'default')
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9_-]+/g, '_')
|
|
63
|
+
.replace(/^_+|_+$/g, '') || 'default';
|
|
64
|
+
const hash = createHash('sha256').update(raw).digest('hex').slice(0, 10);
|
|
65
|
+
const headLength = Math.max(1, maxLength - hash.length - 1);
|
|
66
|
+
return `${base.slice(0, headLength)}_${hash}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldPersistProfiles(): boolean {
|
|
70
|
+
return envFlagEnabled('BROWSER_PERSIST_PROFILE', true);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function shouldPersistSessionState(): boolean {
|
|
74
|
+
return envFlagEnabled('BROWSER_PERSIST_SESSION_STATE', true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveProfileRoot(): string {
|
|
78
|
+
const configured = String(process.env.BROWSER_PROFILE_ROOT || '').trim();
|
|
79
|
+
if (!configured) return ensureWritableDir(BROWSER_PROFILE_ROOT);
|
|
80
|
+
const resolved = path.isAbsolute(configured) ? configured : path.resolve(WORKSPACE_ROOT, configured);
|
|
81
|
+
return ensureWritableDir(resolved);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveCdpUrl(explicit?: string): string | undefined {
|
|
85
|
+
const direct = String(explicit || '').trim();
|
|
86
|
+
if (direct) return direct;
|
|
87
|
+
const configured = String(process.env.BROWSER_CDP_URL || '').trim();
|
|
88
|
+
return configured || undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
47
91
|
function resolveRunner(): BrowserRunner | null {
|
|
48
92
|
if (cachedRunner !== undefined) {
|
|
49
93
|
return cachedRunner;
|
|
@@ -86,12 +130,27 @@ function getSession(sessionId: string): BrowserSession {
|
|
|
86
130
|
}
|
|
87
131
|
|
|
88
132
|
fs.mkdirSync(BROWSER_SOCKET_ROOT, { recursive: true, mode: 0o700 });
|
|
89
|
-
const
|
|
133
|
+
const runtimeKey = deriveStableId(sessionKey, 32);
|
|
134
|
+
const socketDir = path.join(BROWSER_SOCKET_ROOT, runtimeKey);
|
|
90
135
|
fs.mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
91
136
|
|
|
137
|
+
let profileDir: string | undefined;
|
|
138
|
+
if (shouldPersistProfiles()) {
|
|
139
|
+
try {
|
|
140
|
+
profileDir = ensureWritableDir(path.join(resolveProfileRoot(), runtimeKey));
|
|
141
|
+
} catch {
|
|
142
|
+
// Fallback to ephemeral browser context if profile dir cannot be created.
|
|
143
|
+
profileDir = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const stateName = shouldPersistSessionState() ? deriveStableId(sessionKey, 48) : undefined;
|
|
148
|
+
|
|
92
149
|
const session: BrowserSession = {
|
|
93
150
|
sessionKey,
|
|
94
151
|
socketDir,
|
|
152
|
+
profileDir,
|
|
153
|
+
stateName,
|
|
95
154
|
createdAt: Date.now(),
|
|
96
155
|
lastUsedAt: Date.now(),
|
|
97
156
|
};
|
|
@@ -290,24 +349,34 @@ async function runAgentBrowser(
|
|
|
290
349
|
const xdgCacheDir = ensureWritableDir(BROWSER_XDG_CACHE);
|
|
291
350
|
const playwrightBrowsersPath = resolvePlaywrightBrowsersPath();
|
|
292
351
|
const args = [...runner.prefixArgs];
|
|
293
|
-
|
|
294
|
-
|
|
352
|
+
const cdpUrl = resolveCdpUrl(options.cdpUrl);
|
|
353
|
+
if (cdpUrl) {
|
|
354
|
+
args.push('--cdp', cdpUrl);
|
|
295
355
|
}
|
|
296
356
|
args.push('--json', command, ...commandArgs);
|
|
297
357
|
|
|
358
|
+
const browserEnv: NodeJS.ProcessEnv = {
|
|
359
|
+
...process.env,
|
|
360
|
+
AGENT_BROWSER_SOCKET_DIR: session.socketDir,
|
|
361
|
+
AGENT_BROWSER_SESSION: 'default',
|
|
362
|
+
HOME: homeDir,
|
|
363
|
+
XDG_CACHE_HOME: xdgCacheDir,
|
|
364
|
+
NPM_CONFIG_CACHE: npmCacheDir,
|
|
365
|
+
npm_config_cache: npmCacheDir,
|
|
366
|
+
PLAYWRIGHT_BROWSERS_PATH: playwrightBrowsersPath,
|
|
367
|
+
};
|
|
368
|
+
if (session.stateName) {
|
|
369
|
+
browserEnv.AGENT_BROWSER_SESSION_NAME = session.stateName;
|
|
370
|
+
}
|
|
371
|
+
if (!cdpUrl && session.profileDir) {
|
|
372
|
+
browserEnv.AGENT_BROWSER_PROFILE = session.profileDir;
|
|
373
|
+
}
|
|
374
|
+
|
|
298
375
|
try {
|
|
299
376
|
const { stdout, stderr } = await execFileAsync(runner.cmd, args, {
|
|
300
377
|
timeout: timeoutMs,
|
|
301
378
|
maxBuffer: 2 * 1024 * 1024,
|
|
302
|
-
env:
|
|
303
|
-
...process.env,
|
|
304
|
-
AGENT_BROWSER_SOCKET_DIR: session.socketDir,
|
|
305
|
-
HOME: homeDir,
|
|
306
|
-
XDG_CACHE_HOME: xdgCacheDir,
|
|
307
|
-
NPM_CONFIG_CACHE: npmCacheDir,
|
|
308
|
-
npm_config_cache: npmCacheDir,
|
|
309
|
-
PLAYWRIGHT_BROWSERS_PATH: playwrightBrowsersPath,
|
|
310
|
-
},
|
|
379
|
+
env: browserEnv,
|
|
311
380
|
});
|
|
312
381
|
|
|
313
382
|
const output = String(stdout || '').trim();
|
package/docs/index.html
CHANGED
|
@@ -1010,7 +1010,7 @@
|
|
|
1010
1010
|
</div>
|
|
1011
1011
|
<div class="feature-card">
|
|
1012
1012
|
<h3 style="color: var(--accent-1);">Safety Model</h3>
|
|
1013
|
-
<p>Trust-model acceptance is required during onboarding, execution uses runtime guardrails by default, and TUI blocks on unapproved instruction changes.</p>
|
|
1013
|
+
<p>Trust-model acceptance is required during onboarding, execution uses runtime guardrails by default, user-directed browser auth-flow testing is supported, and TUI blocks on unapproved instruction changes.</p>
|
|
1014
1014
|
</div>
|
|
1015
1015
|
<div class="feature-card">
|
|
1016
1016
|
<h3 style="color: var(--accent-1);">Prompt Orchestration</h3>
|
|
@@ -1102,7 +1102,7 @@
|
|
|
1102
1102
|
<div class="tool-pill">
|
|
1103
1103
|
<div class="tool-pill-icon">🖥️</div>
|
|
1104
1104
|
<div class="tool-pill-name">browser_*</div>
|
|
1105
|
-
<div class="tool-pill-desc">Navigate, snapshot, click, type, screenshot, PDF</div>
|
|
1105
|
+
<div class="tool-pill-desc">Navigate, snapshot, click, type, auth-flow testing, screenshot, PDF</div>
|
|
1106
1106
|
</div>
|
|
1107
1107
|
<div class="tool-pill">
|
|
1108
1108
|
<div class="tool-pill-icon">🕑</div>
|
|
@@ -1201,6 +1201,10 @@
|
|
|
1201
1201
|
<div class="faq-q">Is it safe to let the agent run shell commands?</div>
|
|
1202
1202
|
<div class="faq-a">Yes. All tools execute inside ephemeral Docker containers with read-only filesystems, memory caps, and a deny-list of 28+ dangerous command patterns. The host machine is never exposed.</div>
|
|
1203
1203
|
</div>
|
|
1204
|
+
<div class="faq-item">
|
|
1205
|
+
<div class="faq-q">Can browser tools test real login flows?</div>
|
|
1206
|
+
<div class="faq-a">Yes, when explicitly requested by the user for the intended site. Runtime guidance allows authenticated browser-flow testing, while sensitive credential values are redacted from structured audit tool-argument logs.</div>
|
|
1207
|
+
</div>
|
|
1204
1208
|
<div class="faq-item">
|
|
1205
1209
|
<div class="faq-q">Is the audit trail immutable?</div>
|
|
1206
1210
|
<div class="faq-a">Audit logs are append-only and hash-chained per session, so modifications are tamper-evident. Use <code>hybridclaw audit verify <sessionId></code> to validate integrity, and <code>hybridclaw audit approvals --denied</code> to review denied actions.</div>
|
package/package.json
CHANGED
package/src/audit-events.ts
CHANGED
|
@@ -35,6 +35,31 @@ function summarizeToolResult(text: string): string {
|
|
|
35
35
|
return truncateAuditText(text, 280);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const SENSITIVE_ARG_KEY_RE = /(pass(word)?|secret|token|api[_-]?key|authorization|cookie|credential|session)/i;
|
|
39
|
+
|
|
40
|
+
function sanitizeAuditArguments(toolName: string, value: unknown): unknown {
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.map((entry) => sanitizeAuditArguments(toolName, entry));
|
|
43
|
+
}
|
|
44
|
+
if (!value || typeof value !== 'object') {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const out: Record<string, unknown> = {};
|
|
49
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
50
|
+
if (SENSITIVE_ARG_KEY_RE.test(key)) {
|
|
51
|
+
out[key] = '[REDACTED]';
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (toolName === 'browser_type' && key === 'text') {
|
|
55
|
+
out[key] = '[REDACTED]';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
out[key] = sanitizeAuditArguments(toolName, raw);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
export function emitToolExecutionAuditEvents(input: {
|
|
39
64
|
sessionId: string;
|
|
40
65
|
runId: string;
|
|
@@ -44,6 +69,7 @@ export function emitToolExecutionAuditEvents(input: {
|
|
|
44
69
|
toolExecutions.forEach((execution, index) => {
|
|
45
70
|
const toolCallId = `${runId}:tool:${index + 1}`;
|
|
46
71
|
const argumentsObject = parseJsonObject(execution.arguments || '{}');
|
|
72
|
+
const auditArguments = sanitizeAuditArguments(execution.name, argumentsObject);
|
|
47
73
|
|
|
48
74
|
recordAuditEvent({
|
|
49
75
|
sessionId,
|
|
@@ -52,7 +78,7 @@ export function emitToolExecutionAuditEvents(input: {
|
|
|
52
78
|
type: 'tool.call',
|
|
53
79
|
toolCallId,
|
|
54
80
|
toolName: execution.name,
|
|
55
|
-
arguments:
|
|
81
|
+
arguments: auditArguments,
|
|
56
82
|
},
|
|
57
83
|
});
|
|
58
84
|
|
package/src/prompt-hooks.ts
CHANGED
|
@@ -71,6 +71,12 @@ function buildSafetyHook(context: PromptHookContext): string {
|
|
|
71
71
|
'Use bash for execution/build/validation tasks, not for file authoring.',
|
|
72
72
|
'After file changes, run commands only when asked; otherwise explicitly offer to run them immediately.',
|
|
73
73
|
'Only skip file creation when the user explicitly asks for snippet-only or explanation-only output.',
|
|
74
|
+
'',
|
|
75
|
+
'## Browser Auth Handling',
|
|
76
|
+
'When the user explicitly asks for login/auth-flow testing, browser tools may be used on the requested site, including filling credentials and submitting forms.',
|
|
77
|
+
'Do not invent blanket restrictions such as "browser tools are only for public/unauthenticated pages" unless an actual tool/policy error says so.',
|
|
78
|
+
'If earlier assistant messages claimed stricter login limits, treat those as stale and follow this policy and real tool outcomes.',
|
|
79
|
+
'Use provided credentials only for the requested auth flow; do not echo them in prose, write them to files, or send them to unrelated domains.',
|
|
74
80
|
];
|
|
75
81
|
|
|
76
82
|
if (accepted) {
|