typeclaw 0.4.0 → 0.5.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/package.json +1 -1
- package/src/agent/auth.ts +4 -2
- package/src/agent/index.ts +16 -28
- package/src/agent/model-fallback.ts +127 -0
- package/src/agent/tools/curl-impersonate.ts +300 -0
- package/src/agent/tools/ddg.ts +13 -88
- package/src/agent/tools/webfetch/fetch.ts +105 -2
- package/src/agent/tools/webfetch/tool.ts +4 -0
- package/src/bundled-plugins/agent-browser/shim.ts +47 -0
- package/src/bundled-plugins/backup/subagents.ts +2 -0
- package/src/bundled-plugins/memory/README.md +49 -12
- package/src/bundled-plugins/memory/citation-superset.ts +63 -0
- package/src/bundled-plugins/memory/dreaming.ts +105 -17
- package/src/bundled-plugins/memory/index.ts +2 -2
- package/src/bundled-plugins/memory/memory-logger.ts +45 -26
- package/src/bundled-plugins/memory/strength.ts +127 -0
- package/src/bundled-plugins/memory/topics.ts +75 -0
- package/src/bundled-plugins/security/index.ts +87 -43
- package/src/bundled-plugins/security/permissions.ts +36 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +20 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +12 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +23 -3
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +7 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +6 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +9 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +6 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +7 -0
- package/src/channels/adapters/github/index.ts +87 -3
- package/src/channels/router.ts +194 -28
- package/src/channels/types.ts +3 -1
- package/src/cli/init.ts +146 -42
- package/src/cli/model.ts +10 -2
- package/src/cli/oauth-callbacks.ts +49 -0
- package/src/cli/provider.ts +3 -20
- package/src/config/config.ts +59 -24
- package/src/config/models-mutation.ts +42 -8
- package/src/config/providers-mutation.ts +12 -8
- package/src/container/start.ts +18 -1
- package/src/cron/consumer.ts +129 -43
- package/src/init/dockerfile.ts +109 -3
- package/src/init/hatching.ts +2 -2
- package/src/init/index.ts +14 -3
- package/src/init/oauth-login.ts +17 -3
- package/src/permissions/builtins.ts +29 -7
- package/src/permissions/permissions.ts +24 -7
- package/src/plugin/define.ts +2 -0
- package/src/plugin/manager.ts +14 -0
- package/src/plugin/types.ts +6 -0
- package/src/run/index.ts +2 -1
- package/src/skills/typeclaw-memory/SKILL.md +25 -15
- package/src/skills/typeclaw-permissions/SKILL.md +35 -17
- package/src/tui/index.ts +35 -3
- package/src/usage/report.ts +15 -12
- package/typeclaw.schema.json +57 -25
package/src/plugin/define.ts
CHANGED
|
@@ -18,11 +18,13 @@ type DefinePluginSpec<S extends z.ZodType<unknown> | undefined> =
|
|
|
18
18
|
? {
|
|
19
19
|
configSchema: S
|
|
20
20
|
permissions?: readonly string[]
|
|
21
|
+
ownerWildcardExclusions?: readonly string[]
|
|
21
22
|
commands?: Record<string, PluginCommand>
|
|
22
23
|
plugin: (ctx: PluginContext<T>) => Promise<PluginExports>
|
|
23
24
|
}
|
|
24
25
|
: {
|
|
25
26
|
permissions?: readonly string[]
|
|
27
|
+
ownerWildcardExclusions?: readonly string[]
|
|
26
28
|
commands?: Record<string, PluginCommand>
|
|
27
29
|
plugin: (ctx: PluginContext<unknown>) => Promise<PluginExports>
|
|
28
30
|
}
|
package/src/plugin/manager.ts
CHANGED
|
@@ -56,9 +56,11 @@ export async function loadPlugins(opts: LoadPluginsOptions): Promise<LoadPlugins
|
|
|
56
56
|
]
|
|
57
57
|
|
|
58
58
|
const declaredPermissions = collectDeclaredPermissions(allPlugins)
|
|
59
|
+
const ownerWildcardExclusions = collectOwnerWildcardExclusions(allPlugins)
|
|
59
60
|
const permissions = createPermissionService({
|
|
60
61
|
...(opts.roles !== undefined ? { roles: opts.roles } : {}),
|
|
61
62
|
pluginPermissions: declaredPermissions,
|
|
63
|
+
ownerWildcardExclusions,
|
|
62
64
|
})
|
|
63
65
|
|
|
64
66
|
// Non-fatal: surface user-declared `permissions[]` strings that aren't in
|
|
@@ -158,6 +160,18 @@ function collectDeclaredPermissions(
|
|
|
158
160
|
return out
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
function collectOwnerWildcardExclusions(
|
|
164
|
+
plugins: readonly { entry: string; resolved: ResolvedPlugin }[],
|
|
165
|
+
): readonly string[] {
|
|
166
|
+
const out: string[] = []
|
|
167
|
+
for (const { resolved } of plugins) {
|
|
168
|
+
for (const perm of resolved.defined.ownerWildcardExclusions ?? []) {
|
|
169
|
+
if (!out.includes(perm)) out.push(perm)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
export function summarizeLoaded(loaded: LoadPluginsResult['loadedPlugins'], registry: PluginRegistry): string {
|
|
162
176
|
const head = loaded.map((p) => (p.version !== undefined ? `${p.name} v${p.version}` : p.name)).join(', ')
|
|
163
177
|
const counts = [
|
package/src/plugin/types.ts
CHANGED
|
@@ -318,6 +318,12 @@ export type PluginFixResult = {
|
|
|
318
318
|
export type DefinedPlugin<TConfig = never> = {
|
|
319
319
|
readonly configSchema?: z.ZodType<TConfig>
|
|
320
320
|
readonly permissions?: readonly string[]
|
|
321
|
+
// Permission strings the owner wildcard sentinel MUST NOT auto-expand
|
|
322
|
+
// to. Used by the bundled security plugin to keep audience-leak
|
|
323
|
+
// (high-tier) bypasses off the owner role unless an operator grants
|
|
324
|
+
// them explicitly in roles.owner.permissions[]. Generic by design so
|
|
325
|
+
// any future plugin can carve specific permissions out of the wildcard.
|
|
326
|
+
readonly ownerWildcardExclusions?: readonly string[]
|
|
321
327
|
// Declared by-value (not built inside the factory) so the host-stage CLI
|
|
322
328
|
// can dispatch commands without booting plugin runtime state.
|
|
323
329
|
readonly commands?: Record<string, PluginCommand>
|
package/src/run/index.ts
CHANGED
|
@@ -314,7 +314,7 @@ export async function startAgent({
|
|
|
314
314
|
}
|
|
315
315
|
await job.handler(ctx)
|
|
316
316
|
},
|
|
317
|
-
createSessionForCron: async (job) => {
|
|
317
|
+
createSessionForCron: async (job, refOverride) => {
|
|
318
318
|
const snap = pluginRuntime.get()
|
|
319
319
|
const sessionManager = SessionManager.create(cwd, sessionFactory.sessionDir())
|
|
320
320
|
const sessionId = sessionManager.getSessionId()
|
|
@@ -336,6 +336,7 @@ export async function startAgent({
|
|
|
336
336
|
channelRouter: channelManager.router,
|
|
337
337
|
origin: cronOrigin,
|
|
338
338
|
permissions: pluginsLoaded.permissions,
|
|
339
|
+
...(refOverride !== undefined ? { refOverride } : {}),
|
|
339
340
|
...(snap.hasAnyPluginContent
|
|
340
341
|
? {
|
|
341
342
|
plugins: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: typeclaw-memory
|
|
3
|
-
description: Use this skill whenever the user asks what you remember, what you forgot, what you dreamed, why a fact is or isn't in your memory, when memory consolidation happens, or whenever you are about to read or write `MEMORY.md`, anything under `memory/`, or `memory/skills/`. Triggers include "what do you remember", "do you remember X", "forget that", "what did you dream", "when do you dream next", "why did you forget X", "edit MEMORY.md", "add to memory", "your daily streams", "memory-logger", "dreaming", "muscle memory", or any mention of `memory.idleMs` / `memory.dreaming.schedule` in `typeclaw.json`. Read it before you touch any memory file — `MEMORY.md` and `memory/yyyy-MM-dd.jsonl` are runtime-owned, hand-edits are easy to do wrong, and the user almost always means something more specific than "edit memory" when they say it.
|
|
3
|
+
description: Use this skill whenever the user asks what you remember, what you forgot, what you dreamed, why a fact is or isn't in your memory, when memory consolidation happens, or whenever you are about to read or write `MEMORY.md`, anything under `memory/`, or `memory/skills/`. Triggers include "what do you remember", "do you remember X", "forget that", "what did you dream", "when do you dream next", "why did you forget X", "edit MEMORY.md", "add to memory", "your daily streams", "memory-logger", "dreaming", "muscle memory", or any mention of `memory.idleMs` / `memory.bufferBytes` / `memory.dreaming.schedule` in `typeclaw.json`. Read it before you touch any memory file — `MEMORY.md` and `memory/yyyy-MM-dd.jsonl` are runtime-owned, hand-edits are easy to do wrong, and the user almost always means something more specific than "edit memory" when they say it.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# typeclaw-memory
|
|
@@ -13,7 +13,7 @@ This skill exists so you can answer the user's questions about your own memory h
|
|
|
13
13
|
|
|
14
14
|
### Stage 1: memory-logger (online, per-session)
|
|
15
15
|
|
|
16
|
-
After every prompt completes, the runtime fires the `session.idle` hook. The memory plugin starts a debounce timer (`memory.idleMs`, default `
|
|
16
|
+
After every prompt completes, the runtime fires the `session.idle` hook. The memory plugin starts a debounce timer (`memory.idleMs`, default `60_000` ms; minimum `1000`). Every subsequent prompt completion resets the timer. When the user has been quiet for `idleMs`, the plugin spawns the **memory-logger** subagent for the current session. It also fires immediately on `session.end` (websocket close) so the final transcript never gets lost.
|
|
17
17
|
|
|
18
18
|
The memory-logger reads:
|
|
19
19
|
|
|
@@ -45,32 +45,40 @@ When dreaming fires, it reads:
|
|
|
45
45
|
1. `MEMORY.md`
|
|
46
46
|
2. The **undreamed fragments** of every `memory/yyyy-MM-dd.jsonl` (the runtime tells it which fragment ids are new — fragments whose ids are already in `memory/.dreaming-state.json#dreamedThrough[date].dreamedIds` have been consolidated and must NOT be re-cited)
|
|
47
47
|
|
|
48
|
-
It rewrites `MEMORY.md` with the merged result, advances the per-day dreamed-id set in `memory/.dreaming-state.json`, optionally writes muscle-memory skills under `memory/skills/<name>/SKILL.md`, **compacts the touched daily streams** (drops superseded watermarks per source and fragments that are in `dreamedIds` but not cited from `MEMORY.md`), then commits the snapshot with a message shaped like `dream: <summary> <emoji>` — e.g. `dream: 3 fragments + new skill 'pr-review' 🔮`. The summary is derived from the staged diff (line additions in daily streams, newly-added skills, etc.), and the emoji is a random pick from a small thematic pool. After the commit, the runtime sets the `skip-worktree` index flag on the tracked memory artifacts so the user's `git status` and `git diff` stay clean. The flag is cleared and re-applied around every commit.
|
|
48
|
+
It rewrites `MEMORY.md` with the merged result (treating it as a **saturated surface** that gets rebalanced every run, not an append-only log), advances the per-day dreamed-id set in `memory/.dreaming-state.json`, optionally writes muscle-memory skills under `memory/skills/<name>/SKILL.md`, **compacts the touched daily streams** (drops superseded watermarks per source and fragments that are in `dreamedIds` but not cited from `MEMORY.md`), then commits the snapshot with a message shaped like `dream: <summary> <emoji>` — e.g. `dream: 3 fragments + new skill 'pr-review' 🔮`. The summary is derived from the staged diff (line additions in daily streams, newly-added skills, etc.), and the emoji is a random pick from a small thematic pool. After the commit, the runtime sets the `skip-worktree` index flag on the tracked memory artifacts so the user's `git status` and `git diff` stay clean. The flag is cleared and re-applied around every commit.
|
|
49
49
|
|
|
50
50
|
The dreaming subagent has only three tools: `read`, `write`, `ls`. No `bash`. No `edit`. It cannot run shell commands.
|
|
51
51
|
|
|
52
|
+
**Strength-driven rebalancing.** On every run, the runtime computes per-topic strength signals from `MEMORY.md`'s existing citations (`cites`, `days` = distinct calendar days, `last reinforced` date, `age` in days) and injects them as a table at the top of the dreaming user prompt. Dreaming uses them to promote reinforced topics (`days >= 3` → "consistently", `days >= 7` → "always"), merge near-duplicates while preserving the **union** of their fragment ids, and demote decayed single-day topics into a `## Historical observations` bucket as one-line bullets that still cite the underlying fragment. Strong topics (`days >= 3`) are never demoted regardless of age. The bucket grows monotonically — there is no hard-deletion path today; every demoted citation stays alive forever via its bullet.
|
|
53
|
+
|
|
54
|
+
**Citation-superset safety net.** The runtime cross-checks every MEMORY.md rewrite against the prior file's citation set. If dreaming's rewrite drops any previously-cited fragment id, the runtime reverts MEMORY.md to its pre-run bytes, skips fragment GC, but **advances dreamed-ids** anyway (so the same input cannot infinite-loop). The conscious tradeoff: a violation orphans this run's new undreamed fragments — they survive in the daily JSONL (force-committed, recoverable via `git log memory/`) but will never be re-shown to a future dreaming run. If the revert write itself fails, the runtime additionally skips the dreamed-id advance, skips compaction, and skips the commit, leaving recovery to the operator (`git checkout -- MEMORY.md && typeclaw restart`). Look for `[dreaming] citation-superset violation` log lines if `MEMORY.md` ever seems to stop updating.
|
|
55
|
+
|
|
52
56
|
`MEMORY.md` after dreaming looks like:
|
|
53
57
|
|
|
54
58
|
```
|
|
55
59
|
# Memory
|
|
56
60
|
|
|
57
|
-
## <topic>
|
|
61
|
+
## <strong topic — wording from days >= 3>
|
|
58
62
|
<conclusion paragraph in dreaming's own words>
|
|
59
63
|
|
|
60
64
|
fragments:
|
|
61
65
|
- memory/yyyy-MM-dd#<fragment-id>
|
|
62
66
|
- memory/yyyy-MM-dd#<fragment-id>
|
|
63
67
|
|
|
64
|
-
## <topic>
|
|
68
|
+
## <weaker topic>
|
|
65
69
|
<conclusion paragraph>
|
|
66
70
|
|
|
67
71
|
fragments:
|
|
68
72
|
- memory/yyyy-MM-dd#<fragment-id>
|
|
73
|
+
|
|
74
|
+
## Historical observations
|
|
75
|
+
- yyyy-MM-dd: one-line summary of a demoted fact — memory/yyyy-MM-dd#<fragment-id>
|
|
76
|
+
- yyyy-MM-dd: one-line summary of another demoted fact — memory/yyyy-MM-dd#<fragment-id>
|
|
69
77
|
```
|
|
70
78
|
|
|
71
|
-
The first line is always `# Memory`. Topics are level-2 headings. Every topic cites the source fragments by `memory/yyyy-MM-dd#<uuidv7>` (the full id from the fragment event's `id` field) so any claim is traceable back to the daily stream entry that justified it. Citations are id-based, not line-based, so daily streams can be compacted between dreaming runs without invalidating prior references.
|
|
79
|
+
The first line is always `# Memory`. Topics are level-2 headings. Every topic cites the source fragments by `memory/yyyy-MM-dd#<uuidv7>` (the full id from the fragment event's `id` field) so any claim is traceable back to the daily stream entry that justified it. Citations are id-based, not line-based, so daily streams can be compacted between dreaming runs without invalidating prior references. The `## Historical observations` bucket is always last when present.
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
Dreaming does NOT no-op just because there are no new fragments. Even with only watermarks past the tail, if the strength table shows obvious merge or demotion candidates (e.g. a stale single-day topic that has aged past the demotion threshold), the run is productive and rebalances. The truly-no-op case ("only watermarks AND every topic looks well-shaped at its current strength AND no procedure clears the muscle-memory bar") still exits without writing; the watermark advances either way.
|
|
74
82
|
|
|
75
83
|
### What gets injected into your prompt every turn
|
|
76
84
|
|
|
@@ -131,24 +139,26 @@ Stay concrete. Use this map:
|
|
|
131
139
|
|
|
132
140
|
`typeclaw init` does **not** scaffold any of these. They appear when needed — `MEMORY.md` and `memory/` are created by the first dreaming run; daily streams appear when the first memory-logger fires.
|
|
133
141
|
|
|
134
|
-
## When the user asks about `memory.idleMs` or `memory.dreaming.schedule`
|
|
142
|
+
## When the user asks about `memory.idleMs`, `memory.bufferBytes`, or `memory.dreaming.schedule`
|
|
135
143
|
|
|
136
|
-
These are the
|
|
144
|
+
These are the configurable knobs. They live in the `memory` block of `typeclaw.json`:
|
|
137
145
|
|
|
138
146
|
```json
|
|
139
147
|
{
|
|
140
148
|
"memory": {
|
|
141
|
-
"idleMs":
|
|
149
|
+
"idleMs": 60000,
|
|
150
|
+
"bufferBytes": 500000,
|
|
142
151
|
"dreaming": { "schedule": "*/30 * * * *" }
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
154
|
```
|
|
146
155
|
|
|
147
|
-
| Field | Default
|
|
148
|
-
| -------------------------- |
|
|
149
|
-
| `memory.idleMs` | `
|
|
150
|
-
| `memory.
|
|
151
|
-
| `memory.dreaming
|
|
156
|
+
| Field | Default | Effect | Reload class |
|
|
157
|
+
| -------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
|
158
|
+
| `memory.idleMs` | `60000` (min `1000`) | Debounce window before `memory-logger` spawns after a prompt completes. | Restart-required. |
|
|
159
|
+
| `memory.bufferBytes` | `500000` (0 disables) | Size-based ceiling. Spawns `memory-logger` immediately when the transcript has grown by this many bytes since the last run, regardless of `idleMs`. Lets busy channel sessions still produce memory updates without waiting for a full quiet window. Minimum `10000` when non-zero. | Restart-required. |
|
|
160
|
+
| `memory.dreaming` | `{}` (cron job on) | Dreaming cron job is always registered. Override `schedule` to change when it fires. | Restart-required. |
|
|
161
|
+
| `memory.dreaming.schedule` | `"*/30 * * * *"` | Cron expression. Parsed via `cron-parser`; an invalid expression fails config load. Fires with nothing past the watermark short-circuit before any LLM call, so frequent no-op fires are intentionally cheap. | Restart-required. |
|
|
152
162
|
|
|
153
163
|
Both fields are restart-required because plugin config is read once at boot. After editing them, tell the user: "Edited `memory.<field>` — restart-required. Run `typeclaw restart` (host stage) to pick up the change." The bundled plugin's config schema is merged into `typeclaw.schema.json`, so editor autocomplete will validate these fields, but a `reload` will not re-instantiate the plugin.
|
|
154
164
|
|
|
@@ -21,12 +21,12 @@ Each role carries a set of **permissions** — opaque dotted strings like `chann
|
|
|
21
21
|
|
|
22
22
|
You always have these four, even if `typeclaw.json` declares zero `roles`. User-declared roles **append** match rules to the built-ins but **replace** the permission list entirely (so `"permissions": []` on a built-in role means "no permissions" — be careful).
|
|
23
23
|
|
|
24
|
-
| Role | Built-in `match[]` | Default `permissions[]`
|
|
25
|
-
| --------- | ----------------------------------------------------------------- |
|
|
26
|
-
| `owner` | `["tui"]` (always prepended) | `channel.respond`, `cron.schedule`, `cron.modify`, **
|
|
27
|
-
| `trusted` | none | `channel.respond`, `cron.schedule`, `security.bypass.
|
|
28
|
-
| `member` | none | `channel.respond`
|
|
29
|
-
| `guest` | none (fallback when nothing else matches, or stamped role is bad) | none
|
|
24
|
+
| Role | Built-in `match[]` | Default `permissions[]` |
|
|
25
|
+
| --------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `owner` | `["tui"]` (always prepended) | `channel.respond`, `cron.schedule`, `cron.modify`, `security.bypass.low`, `security.bypass.medium`, **plugin-contributed `security.bypass.*` MINUS high-tier strings** (the wildcard sentinel expands to plugin bypasses but excludes the security plugin's `ownerWildcardExclusions` — today: `gitExfil`, `gitRemoteTainted`, `outboundSecret`, `systemPromptLeak`, plus `bypass.high`) |
|
|
27
|
+
| `trusted` | none | `channel.respond`, `cron.schedule`, `security.bypass.low` |
|
|
28
|
+
| `member` | none | `channel.respond` |
|
|
29
|
+
| `guest` | none (fallback when nothing else matches, or stamped role is bad) | none |
|
|
30
30
|
|
|
31
31
|
A session that doesn't match anything resolves to `guest`. `guest` has no `channel.respond`, so the router silently drops inbound messages whose author resolves to `guest`. **This is the most common cause of "the agent stopped responding"**: the user added a channel but did not add a match rule, so every speaker in that channel is `guest` and every inbound is dropped before you ever see it. There is no message in your session log when this happens — only a host-side line `[channels] <key>: denied by permissions (channel.respond) author=<id>`.
|
|
32
32
|
|
|
@@ -79,10 +79,24 @@ Things the DSL rejects (the parser emits actionable errors at boot, but you shou
|
|
|
79
79
|
Three sources contribute permission strings:
|
|
80
80
|
|
|
81
81
|
1. **Core** (always present): `channel.respond`, `cron.schedule`, `cron.modify`.
|
|
82
|
-
2. **Bundled security plugin** (always loaded): `security.bypass.secretExfilBash`, `security.bypass.gitExfil`, `security.bypass.gitRemoteTainted`, `security.bypass.secretExfilRead`, `security.bypass.ssrf`, `security.bypass.sessionSearchSecrets`, `security.bypass.systemPromptLeak`, `security.bypass.outboundSecret
|
|
82
|
+
2. **Bundled security plugin** (always loaded): the eight per-guard strings (`security.bypass.secretExfilBash`, `security.bypass.gitExfil`, `security.bypass.gitRemoteTainted`, `security.bypass.secretExfilRead`, `security.bypass.ssrf`, `security.bypass.sessionSearchSecrets`, `security.bypass.systemPromptLeak`, `security.bypass.outboundSecret`) AND three severity-tier strings (`security.bypass.low`, `security.bypass.medium`, `security.bypass.high`).
|
|
83
83
|
3. **User-declared plugins** (variable): each plugin can contribute its own strings via `definePlugin({ permissions: [...] })`.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
The security plugin classifies each guard on a two-axis policy:
|
|
86
|
+
|
|
87
|
+
- **high — audience-leak.** Bypass sends data to a third-party audience outside the operator's control loop (channel readers, remote git hosts). Inhabitants: `outboundSecret`, `systemPromptLeak`, `gitExfil`, `gitRemoteTainted`. **No role auto-bypasses high.** Per-call ack required from every role, including `owner`. The canonical case is **owner-in-public-channel**: even an owner asking "post deploy status to #general" must not silently include a `Bearer ghp_…` line; even `git push` from TUI must be ack'd. Operators who knowingly want one role to skip a high-tier guard add the per-guard string explicitly to `roles.<role>.permissions[]`.
|
|
88
|
+
- **medium — silent-attack.** Bypass returns secrets / IAM creds into model context with no immediate operator visibility. Inhabitants: `secretExfilBash`, `secretExfilRead`, `ssrf`, `sessionSearchSecrets`. `owner` bypasses (operator already has host access); `trusted` does NOT.
|
|
89
|
+
- **low — noisy, immediately recoverable.** No inhabitants today. Forward-compat for future guards. `trusted` carries `bypass.low` so a future low-tier guard auto-bypasses for trusted without a config edit.
|
|
90
|
+
|
|
91
|
+
At `tool.before` time, an actor bypasses a guard if they hold **either** the tier permission **or** the per-guard permission (OR-check, both axes work forever).
|
|
92
|
+
|
|
93
|
+
`owner` carries `security.bypass.low + security.bypass.medium` AND the wildcard sentinel; the sentinel expands to plugin-contributed `security.bypass.*` strings minus the security plugin's `ownerWildcardExclusions` (today: the four high-tier per-guard strings plus `bypass.high`). Net: `owner` auto-bypasses every low- and medium-tier guard; high-tier guards require ack from owner too. `trusted` carries only `bypass.low` — no per-guard medium/high grants by default. `member` and `guest` carry no `security.bypass.*` strings.
|
|
94
|
+
|
|
95
|
+
**Trusted lost two per-guard grants in PR #255 compared to pre-PR behavior.** Before: trusted carried `bypassSecretExfilBash` and `bypassGitExfil` so they could `bash env` and `git push` without acks. After: those guards (medium and high respectively) need acks for trusted too. Operators who relied on the old ergonomics can restore them by adding the per-guard strings explicitly to `roles.trusted.permissions[]` in `typeclaw.json` — the per-guard path is supported forever via the OR-check. When the user complains "trusted can't push anymore," this is the explanation and the workaround.
|
|
96
|
+
|
|
97
|
+
Note on the two-step `gitRemoteTainted` defense: even when an operator explicitly grants `security.bypass.gitExfil` to a role (re-opening the high-tier bypass for `git push`), the second-step `gitRemoteTainted` check still fires on the eventual push if origin was re-pointed mid-session. The recorder runs on the first step (the `set-url`) gated by "would the command actually run" — so the second-step checker has taint state to consult. Granting `bypassGitExfil` does NOT silently grant `bypassGitRemoteTainted`; those are independent per-guard strings AND independent high-tier guards.
|
|
98
|
+
|
|
99
|
+
**Two-layer defense for channel-side git operations**: the runtime `tool.before` guards are not the only layer that gates `git push` from channel messages. The security plugin's `session.prompt` hook also pattern-matches inbound text for `git push` / `git remote add` / `gh repo create --push` and injects a refusal rule into the system prompt. **The prompt-side `git_exfil` defense is gated to non-subagent origins** — it fires for `channel` and `tui` prompts but skips `subagent` prompts. The reason: bundled subagents like `backup-diagnose` legitimately embed git stderr in their payloads (which contains literal "git push --help" hint strings on failures), and triggering the defense there would inject a "do NOT run git push" rule that contradicts the subagent's own system-prompt instructions to retry with an ack. The runtime `tool.before` is the universal backstop for subagents (under the audience-leak policy, even owner-spawned subagents need an ack for `git push`), so the prompt-side check is redundant for them and harmful to bundled-plugin recovery flows. For channel and TUI prompts the two layers agree: nobody auto-bypasses gitExfil at the runtime layer, so the prompt-injection layer's text-match refusal is the same answer the runtime would give. The only case where the two layers disagree is when an operator has explicitly granted `security.bypass.gitExfil` to a channel speaker's role in `typeclaw.json` — then the runtime would allow the push but the prompt-injection text-match would still refuse. That's a known narrow-scope gap (operator opted into the bypass already); if the user is confused why the agent refused a channel-side push despite the per-guard grant they added, this is why.
|
|
86
100
|
|
|
87
101
|
User-declared `permissions[]` strings that don't appear in any of the three sources are **logged as warnings at boot** (`[permissions] role "X" declares unknown permission "Y" — did you mean 'Z'?`) but the role still resolves with the unknown string in its list. This is intentional — the runtime is forward-compatible with strings from plugins that aren't loaded yet — but it also means typos silently fail to bypass guards. If you wrote `security.bypass.secretExfilBach` instead of `Bash`, no guard will be skipped and you will only notice when you read the boot logs.
|
|
88
102
|
|
|
@@ -93,18 +107,19 @@ The security plugin's `tool.before` hook produces block messages of the form:
|
|
|
93
107
|
```
|
|
94
108
|
Guard `<guardName>` blocked <what>. If this is genuinely intentional and the user
|
|
95
109
|
explicitly asked for it, retry with `acknowledgeGuards.<guardName>: true` in the
|
|
96
|
-
<tool> arguments. Or run as a role carrying `<permission>` (
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
<tool> arguments. Or run as a role carrying `<per-guard-permission>` (...role hint...)
|
|
111
|
+
or the tier permission `security.bypass.<low|medium|high>`; see the
|
|
112
|
+
`typeclaw-permissions` skill.
|
|
99
113
|
```
|
|
100
114
|
|
|
101
|
-
|
|
115
|
+
Four escape hatches, ordered from least to most invasive:
|
|
102
116
|
|
|
103
117
|
1. **`acknowledgeGuards.<guardName>: true`** in the tool args. This is a per-call, in-session bypass. Use it when the user has just explicitly told you to run the dangerous thing (e.g. "yes, push the secret to a private gist on purpose"). Never use it without explicit user confirmation — the guard exists for a reason.
|
|
104
|
-
2. **Run as a role with the bypass permission**. If the user wants this pattern to keep working without an ack every time, they edit `roles.<role>.permissions[]` to include the `security.bypass.<
|
|
105
|
-
3. **Run
|
|
118
|
+
2. **Run as a role with the per-guard bypass permission**. If the user wants this pattern to keep working without an ack every time, they edit `roles.<role>.permissions[]` to include the specific `security.bypass.<guardName>` string the block message named. This is the most granular grant — it only opens up that one guard. Use this when the user wants exactly one capability and nothing else.
|
|
119
|
+
3. **Run as a role with the tier bypass permission**. The block message also names the tier permission (`security.bypass.low` / `.medium` / `.high`). Granting the tier opens up every guard of that tier at once — broader than option 2, narrower than full owner. Use this when "let trusted users post credentials to chat AND view system prompt fingerprints AND search session history" is the user's actual intent rather than three separate per-guard grants.
|
|
120
|
+
4. **Run from a session that already resolves to a role with the bypass**. The TUI is always `owner`, so a guard that blocks in channel sessions for a `member` author will not block at all from the TUI. This is why "the agent can do X in TUI but not in Slack" is normal, not a bug.
|
|
106
121
|
|
|
107
|
-
When you see a block, tell the user **which permission would skip it** (the block message now names
|
|
122
|
+
When you see a block, tell the user **which permission would skip it** (the block message now names both the per-guard and the tier options) and **which built-in roles have those permissions**. Do not just relay the guard reason — that loses the access-control framing entirely.
|
|
108
123
|
|
|
109
124
|
## When the user asks "why aren't you replying in #channel?"
|
|
110
125
|
|
|
@@ -121,7 +136,7 @@ To distinguish cause 1/2 from cause 3: if `typeclaw logs <container> -f` (host s
|
|
|
121
136
|
This is a `roles` edit. The full procedure:
|
|
122
137
|
|
|
123
138
|
1. **Resolve the coordinates.** Get the platform name (`slack | discord | telegram | kakao`), the workspace ID, the chat ID. If the user gave you names, ask them or look them up in the participants list of a previous inbound from that channel.
|
|
124
|
-
2. **Pick a role.** Default to `member` for "give them normal channel access". Use `trusted` if they should also be able to schedule cron
|
|
139
|
+
2. **Pick a role.** Default to `member` for "give them normal channel access". Use `trusted` if they should also be able to schedule cron — by default trusted gets ONLY `bypass.low` (no inhabitants today), so trusted on its own does NOT skip any security guard. If the user wants the old pre-PR-#255 trusted ergonomics (bypass bash secret guard, push without ack), add per-guard strings explicitly: `roles.trusted.permissions: ["channel.respond", "cron.schedule", "security.bypass.low", "security.bypass.secretExfilBash", "security.bypass.gitExfil"]`. Use `owner` only for the primary operator — owner auto-bypasses every medium-tier guard (`secretExfilBash`, `secretExfilRead`, `ssrf`, `sessionSearchSecrets`) but **still must ack every high-tier guard** (`gitExfil`, `gitRemoteTainted`, `outboundSecret`, `systemPromptLeak`) because audience-leak guards have no role auto-bypass — that's the owner-in-public-channel rule. If the user explicitly wants `git push` from TUI without acks, that's a per-guard explicit grant on `roles.owner.permissions[]` (re-add `security.bypass.gitExfil`), and the user should understand they are re-opening the audience-leak path for that guard.
|
|
125
140
|
3. **Edit `typeclaw.json` `roles.<role>.match[]`.** Append the canonical DSL string. Example: `roles.member.match` adds `"slack:T0123/C0ABCDE"`. If the user wants only a specific person in that channel, append `slack:T0123/C0ABCDE author:U_ME` instead.
|
|
126
141
|
4. **Restart.** `roles` is **restart-required** — `typeclaw reload` does not re-evaluate role config. Tell the user: "edited `roles.<role>.match` — restart-required. Run `typeclaw restart` (host stage)."
|
|
127
142
|
5. **Commit the change.** See the `typeclaw-git` skill. The decision context in the commit message should name the role, the channel, and the author/scope ("let @X talk to me as `member` in #foo in workspace bar").
|
|
@@ -151,7 +166,10 @@ If you see a cron job mysteriously failing every fire with `denied by permission
|
|
|
151
166
|
## Things you must not do
|
|
152
167
|
|
|
153
168
|
- **Do not write `*` in user-declared `permissions[]`.** The owner wildcard is a runtime sentinel, not part of the user-facing string format. The schema rejects `*` (it's not a valid dotted permission string anyway).
|
|
154
|
-
- **Do not invent permission strings.** Only the three sources above (core, security plugin, declared plugins) contribute valid strings. A string like `bash.execute` looks plausible but is not gated by anything and will only earn a boot warning. If the user asks for a permission the model doesn't have, tell them — don't invent one.
|
|
169
|
+
- **Do not invent permission strings.** Only the three sources above (core, security plugin including the eight per-guard + three tier strings, declared plugins) contribute valid strings. A string like `bash.execute` looks plausible but is not gated by anything and will only earn a boot warning. If the user asks for a permission the model doesn't have, tell them — don't invent one.
|
|
170
|
+
|
|
171
|
+
- **Do not grant `security.bypass.high` casually.** High-tier guards (`gitExfil`, `gitRemoteTainted`, `outboundSecret`, `systemPromptLeak`) all defend the audience-leak axis — bypassing them means data leaves the operator's perimeter without per-call confirmation. Even `owner` doesn't have `bypass.high` by default for this exact reason. Granting `security.bypass.high` to any role opens audience-leak bypass on every current high-tier guard PLUS every future high-tier guard added by a security plugin update. If the user wants one specific high-tier bypass (e.g. "let me push from TUI without acks"), grant the **per-guard** string explicitly (`security.bypass.gitExfil`) on the specific role, not the tier — that's narrower and won't widen on plugin updates.
|
|
172
|
+
- **Do not grant `security.bypass.medium` to roles wider than the operator.** Medium-tier bypasses (`secretExfilBash`, `secretExfilRead`, `ssrf`, `sessionSearchSecrets`) silently dump secrets into model context. Granting `bypass.medium` to a `trusted` role that matches a broad Slack workspace means any trusted speaker can ask the agent to dump environment variables and the bypass succeeds — the operator only sees on session review. If the user wants this anyway, name the specific guard with a per-guard grant rather than the whole tier.
|
|
155
173
|
- **Do not promise that `typeclaw reload` applied a `roles` edit.** `roles` is restart-required. The reload tool will return success on the config file change, but the live `PermissionService` was built at boot and is not swapped on reload.
|
|
156
174
|
- **Do not silently change a built-in role's permission list.** Setting `"permissions": []` on `member` is a wholesale replace, not a merge — you just took `channel.respond` away from every speaker who resolves to `member`. If the user said "give member just `channel.respond` and nothing else", that's fine (it's the same as the default), but say so explicitly: "this matches the default for `member`, no behavior change". If the user said "remove cron from `trusted`", make the change but warn that `trusted` no longer carries `cron.schedule` either.
|
|
157
175
|
- **Do not write match rules using display names** (`#general`, `@user`, channel/user names). Match rules are platform IDs. Display names change; IDs don't. Always look up the ID before writing the rule.
|
package/src/tui/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Editor, Key, Markdown, matchesKey, ProcessTerminal, type Terminal, Text, TUI } from '@mariozechner/pi-tui'
|
|
2
2
|
|
|
3
|
+
import { parseCommand } from '@/commands'
|
|
4
|
+
|
|
3
5
|
import { createClient as createClientDefault, type Client } from './client'
|
|
4
6
|
import { formatQueuePanel, formatToolEnd, formatToolStart, formatUserPromptHistory } from './format'
|
|
5
7
|
import { colors, editorTheme, markdownTheme } from './theme'
|
|
@@ -9,6 +11,21 @@ export type TerminalFactory = () => Terminal
|
|
|
9
11
|
|
|
10
12
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000
|
|
11
13
|
|
|
14
|
+
// Bare slash-command names (no leading `/`) the TUI intercepts client-side and
|
|
15
|
+
// turns into a clean process exit. The hatching ritual tells the agent to point
|
|
16
|
+
// users at `/quit` (see src/init/hatching.ts); without an intercept the literal
|
|
17
|
+
// text would be shipped to the LLM as a chat message. Grammar (case-insensitive,
|
|
18
|
+
// whitespace-tolerant, `//foo` escapes to a literal prompt) comes from
|
|
19
|
+
// `parseCommand` in src/commands so channel and TUI slash commands stay
|
|
20
|
+
// consistent. Arguments after the name disqualify the match: `/quit me a story`
|
|
21
|
+
// is a real prompt, not a command.
|
|
22
|
+
const QUIT_COMMAND_NAMES: ReadonlySet<string> = new Set(['quit', 'exit'])
|
|
23
|
+
|
|
24
|
+
function isQuitCommand(text: string): boolean {
|
|
25
|
+
const parsed = parseCommand(text)
|
|
26
|
+
return parsed !== null && parsed.args.length === 0 && QUIT_COMMAND_NAMES.has(parsed.name)
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
export type VersionMismatch = { expected: string; actual: string }
|
|
13
30
|
|
|
14
31
|
export type TuiOptions = {
|
|
@@ -205,14 +222,18 @@ export function createTui({
|
|
|
205
222
|
return undefined
|
|
206
223
|
})
|
|
207
224
|
|
|
225
|
+
const shutdown = (code: number) => {
|
|
226
|
+
tui.stop()
|
|
227
|
+
client.close()
|
|
228
|
+
exit(code)
|
|
229
|
+
}
|
|
230
|
+
|
|
208
231
|
// Ctrl+C exits cleanly. In raw mode the kernel does NOT generate SIGINT,
|
|
209
232
|
// so we must intercept the \x03 byte ourselves. The Editor would otherwise
|
|
210
233
|
// swallow it. tui.stop() restores raw-mode/cursor/echo before we exit.
|
|
211
234
|
tui.addInputListener((data) => {
|
|
212
235
|
if (matchesKey(data, Key.ctrl('c'))) {
|
|
213
|
-
|
|
214
|
-
client.close()
|
|
215
|
-
exit(0)
|
|
236
|
+
shutdown(0)
|
|
216
237
|
return { consume: true }
|
|
217
238
|
}
|
|
218
239
|
return undefined
|
|
@@ -220,6 +241,10 @@ export function createTui({
|
|
|
220
241
|
|
|
221
242
|
editor.onSubmit = (text) => {
|
|
222
243
|
if (text.trim().length === 0) return
|
|
244
|
+
if (isQuitCommand(text)) {
|
|
245
|
+
shutdown(0)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
223
248
|
editor.setText('')
|
|
224
249
|
editor.addToHistory(text)
|
|
225
250
|
tui.requestRender()
|
|
@@ -238,6 +263,13 @@ export function createTui({
|
|
|
238
263
|
}
|
|
239
264
|
|
|
240
265
|
if (initialPrompt) {
|
|
266
|
+
// initialPrompt bypasses editor.onSubmit, so the quit intercept above
|
|
267
|
+
// would never run. Guard the same way so `typeclaw tui /quit` exits
|
|
268
|
+
// instead of leaking the command into the agent's chat context.
|
|
269
|
+
if (isQuitCommand(initialPrompt)) {
|
|
270
|
+
shutdown(0)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
241
273
|
await send(initialPrompt)
|
|
242
274
|
}
|
|
243
275
|
|
package/src/usage/report.ts
CHANGED
|
@@ -145,27 +145,30 @@ function renderOriginLabel(kind: OriginKind, ctx: RenderCtx): string {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// Sparkline trend across the full byDay range, scaled to the row's max
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
148
|
+
// Sparkline trend across the full byDay range, scaled to the row's max token
|
|
149
|
+
// count. Tokens (not cost) drive the chart because they're the load-bearing
|
|
150
|
+
// usage signal — model price changes and free-tier credits shouldn't flatten
|
|
151
|
+
// or distort the visible workload pattern. Returns null when there are fewer
|
|
152
|
+
// than 2 days (a single point conveys no trend information). The 8-level
|
|
153
|
+
// Unicode block scale `▁▂▃▄▅▆▇█` lets us pack ~80 days into a one-line glance
|
|
154
|
+
// — wider than any table-based view could fit at terminal widths under ~160
|
|
155
|
+
// columns.
|
|
153
156
|
const SPARK_GLYPHS = '▁▂▃▄▅▆▇█'
|
|
154
157
|
|
|
155
|
-
function renderDailyTrend(byDay: readonly { date: string;
|
|
158
|
+
function renderDailyTrend(byDay: readonly { date: string; totalTokens: number }[], ctx: RenderCtx): string | null {
|
|
156
159
|
if (byDay.length < 2) return null
|
|
157
|
-
const
|
|
158
|
-
const max =
|
|
160
|
+
const tokens = byDay.map((d) => d.totalTokens)
|
|
161
|
+
const max = tokens.reduce((m, t) => Math.max(m, t), 0)
|
|
159
162
|
if (max <= 0) return null
|
|
160
|
-
const spark =
|
|
161
|
-
.map((
|
|
162
|
-
const idx = Math.min(SPARK_GLYPHS.length - 1, Math.max(0, Math.round((
|
|
163
|
+
const spark = tokens
|
|
164
|
+
.map((t) => {
|
|
165
|
+
const idx = Math.min(SPARK_GLYPHS.length - 1, Math.max(0, Math.round((t / max) * (SPARK_GLYPHS.length - 1))))
|
|
163
166
|
return SPARK_GLYPHS[idx]!
|
|
164
167
|
})
|
|
165
168
|
.join('')
|
|
166
169
|
const first = byDay[0]!.date
|
|
167
170
|
const last = byDay[byDay.length - 1]!.date
|
|
168
|
-
return `${dim('Trend (
|
|
171
|
+
return `${dim('Trend (tokens):', ctx)} ${color('cyan', spark, ctx)} ${dim(`${first} → ${last}`, ctx)}`
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
function renderDaily(report: UsageReport, ctx: RenderCtx, limit: number | undefined): string {
|
package/typeclaw.schema.json
CHANGED
|
@@ -12,33 +12,59 @@
|
|
|
12
12
|
"maximum": 65535
|
|
13
13
|
},
|
|
14
14
|
"models": {
|
|
15
|
-
"default": {
|
|
16
|
-
"default": "openai/gpt-5.4-nano"
|
|
17
|
-
},
|
|
18
15
|
"type": "object",
|
|
19
16
|
"propertyNames": {
|
|
20
17
|
"type": "string",
|
|
21
18
|
"minLength": 1
|
|
22
19
|
},
|
|
23
20
|
"additionalProperties": {
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
"anyOf": [
|
|
22
|
+
{
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": [
|
|
25
|
+
"openai/gpt-5.4-nano",
|
|
26
|
+
"openai/gpt-5.4-mini",
|
|
27
|
+
"openai/gpt-5.4",
|
|
28
|
+
"openai/gpt-5.5",
|
|
29
|
+
"openai-codex/gpt-5.4-mini",
|
|
30
|
+
"openai-codex/gpt-5.4",
|
|
31
|
+
"openai-codex/gpt-5.5",
|
|
32
|
+
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
33
|
+
"zai/glm-4.5-air",
|
|
34
|
+
"zai/glm-4.6",
|
|
35
|
+
"zai/glm-4.7",
|
|
36
|
+
"zai-coding/glm-4.5-air",
|
|
37
|
+
"zai-coding/glm-4.7",
|
|
38
|
+
"zai-coding/glm-5",
|
|
39
|
+
"zai-coding/glm-5-turbo",
|
|
40
|
+
"zai-coding/glm-5.1"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"minItems": 1,
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"enum": [
|
|
49
|
+
"openai/gpt-5.4-nano",
|
|
50
|
+
"openai/gpt-5.4-mini",
|
|
51
|
+
"openai/gpt-5.4",
|
|
52
|
+
"openai/gpt-5.5",
|
|
53
|
+
"openai-codex/gpt-5.4-mini",
|
|
54
|
+
"openai-codex/gpt-5.4",
|
|
55
|
+
"openai-codex/gpt-5.5",
|
|
56
|
+
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
57
|
+
"zai/glm-4.5-air",
|
|
58
|
+
"zai/glm-4.6",
|
|
59
|
+
"zai/glm-4.7",
|
|
60
|
+
"zai-coding/glm-4.5-air",
|
|
61
|
+
"zai-coding/glm-4.7",
|
|
62
|
+
"zai-coding/glm-5",
|
|
63
|
+
"zai-coding/glm-5-turbo",
|
|
64
|
+
"zai-coding/glm-5.1"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
42
68
|
]
|
|
43
69
|
}
|
|
44
70
|
},
|
|
@@ -898,6 +924,7 @@
|
|
|
898
924
|
"tmux": true,
|
|
899
925
|
"cjkFonts": true,
|
|
900
926
|
"cloudflared": true,
|
|
927
|
+
"xvfb": true,
|
|
901
928
|
"append": []
|
|
902
929
|
}
|
|
903
930
|
},
|
|
@@ -911,6 +938,7 @@
|
|
|
911
938
|
"tmux": true,
|
|
912
939
|
"cjkFonts": true,
|
|
913
940
|
"cloudflared": true,
|
|
941
|
+
"xvfb": true,
|
|
914
942
|
"append": []
|
|
915
943
|
},
|
|
916
944
|
"type": "object",
|
|
@@ -963,6 +991,10 @@
|
|
|
963
991
|
"default": true,
|
|
964
992
|
"type": "boolean"
|
|
965
993
|
},
|
|
994
|
+
"xvfb": {
|
|
995
|
+
"default": true,
|
|
996
|
+
"type": "boolean"
|
|
997
|
+
},
|
|
966
998
|
"append": {
|
|
967
999
|
"default": [],
|
|
968
1000
|
"type": "array",
|
|
@@ -1130,20 +1162,20 @@
|
|
|
1130
1162
|
},
|
|
1131
1163
|
"memory": {
|
|
1132
1164
|
"default": {
|
|
1133
|
-
"idleMs":
|
|
1134
|
-
"bufferBytes":
|
|
1165
|
+
"idleMs": 60000,
|
|
1166
|
+
"bufferBytes": 500000,
|
|
1135
1167
|
"spawnTimeoutMs": 50000
|
|
1136
1168
|
},
|
|
1137
1169
|
"type": "object",
|
|
1138
1170
|
"properties": {
|
|
1139
1171
|
"idleMs": {
|
|
1140
|
-
"default":
|
|
1172
|
+
"default": 60000,
|
|
1141
1173
|
"type": "integer",
|
|
1142
1174
|
"minimum": 1000,
|
|
1143
1175
|
"maximum": 9007199254740991
|
|
1144
1176
|
},
|
|
1145
1177
|
"bufferBytes": {
|
|
1146
|
-
"default":
|
|
1178
|
+
"default": 500000,
|
|
1147
1179
|
"type": "integer",
|
|
1148
1180
|
"minimum": 0,
|
|
1149
1181
|
"maximum": 9007199254740991
|