crewly 1.20.279 → 1.20.281
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/config/hooks/live-checkout-guard/guard.mjs +452 -0
- package/config/skills/agent/core/reply/SKILL.md +2 -1
- package/config/skills/agent/core/reply/execute.sh +11 -3
- package/config/skills/agent/core/reply/execute.test.sh +4 -0
- package/dist/backend/backend/src/constants.d.ts +81 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +89 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts +1 -0
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js +3 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts +5 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js +42 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.d.ts +72 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.js +113 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +111 -1
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +4 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts +4 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.js +11 -2
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/credential-guard.service.d.ts +3 -2
- package/dist/backend/backend/src/services/agent/credential-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/credential-guard.service.js +12 -4
- package/dist/backend/backend/src/services/agent/credential-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.js +7 -0
- package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.d.ts +45 -0
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.js +109 -0
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.js +4 -1
- package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +13 -7
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-agent.service.d.ts +16 -0
- package/dist/backend/backend/src/services/drive/drive-agent.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-agent.service.js +37 -5
- package/dist/backend/backend/src/services/drive/drive-agent.service.js.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.d.ts +172 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.d.ts.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.js +301 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.js.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-sync.service.d.ts +112 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-sync.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-sync.service.js +247 -0
- package/dist/backend/backend/src/services/drive/drive-briefing-sync.service.js.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.contract.d.ts +88 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.contract.d.ts.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.contract.js +18 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.contract.js.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.wiring.d.ts +73 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.wiring.d.ts.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.wiring.js +131 -0
- package/dist/backend/backend/src/services/drive/drive-briefing.wiring.js.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-cloud.contract.d.ts +9 -2
- package/dist/backend/backend/src/services/drive/drive-cloud.contract.d.ts.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-cloud.contract.js +9 -4
- package/dist/backend/backend/src/services/drive/drive-cloud.contract.js.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-keep-warm.d.ts +72 -0
- package/dist/backend/backend/src/services/drive/drive-keep-warm.d.ts.map +1 -0
- package/dist/backend/backend/src/services/drive/drive-keep-warm.js +109 -0
- package/dist/backend/backend/src/services/drive/drive-keep-warm.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/owner-hook-message.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-hook-message.js +11 -5
- package/dist/backend/backend/src/services/messaging/owner-hook-message.js.map +1 -1
- package/dist/backend/backend/src/utils/boot-owner-alert.d.ts +33 -0
- package/dist/backend/backend/src/utils/boot-owner-alert.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/boot-owner-alert.js +60 -0
- package/dist/backend/backend/src/utils/boot-owner-alert.js.map +1 -0
- package/dist/backend/backend/src/utils/build-provenance.d.ts +38 -0
- package/dist/backend/backend/src/utils/build-provenance.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/build-provenance.js +63 -1
- package/dist/backend/backend/src/utils/build-provenance.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +81 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +89 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +4 -2
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts +4 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.js +11 -2
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/credential-guard.service.d.ts +3 -2
- package/dist/cli/backend/src/services/agent/credential-guard.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/credential-guard.service.js +12 -4
- package/dist/cli/backend/src/services/agent/credential-guard.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.d.ts +45 -0
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.js +109 -0
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.js +4 -1
- package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +13 -7
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/cli/backend/src/services/drive/drive-keep-warm.d.ts +72 -0
- package/dist/cli/backend/src/services/drive/drive-keep-warm.d.ts.map +1 -0
- package/dist/cli/backend/src/services/drive/drive-keep-warm.js +109 -0
- package/dist/cli/backend/src/services/drive/drive-keep-warm.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Crewly live-checkout guard — a pre-tool hook that stops an agent changing the
|
|
3
|
+
// LIVE Crewly checkout (the one the running Crewly was built and is served from).
|
|
4
|
+
//
|
|
5
|
+
// Incident (2026-10-08/09): an agent edited files in, then `git switch`ed and
|
|
6
|
+
// committed in, the live checkout. On the next restart Crewly refused to start
|
|
7
|
+
// ("STALE BUILD: ... HEAD is ...") and was down until an admin switched back.
|
|
8
|
+
//
|
|
9
|
+
// Usage (wired by the backend; PreToolUse JSON on stdin):
|
|
10
|
+
// node guard.mjs <live-root> [<live-root> ...]
|
|
11
|
+
//
|
|
12
|
+
// Judged:
|
|
13
|
+
// - Bash (also Codex "Bash"): git state-changing subcommands whose target is a
|
|
14
|
+
// live root (`cd <live> && git ...`, `git -C <live> ...`, `--git-dir`,
|
|
15
|
+
// `--work-tree`, or the call's cwd being the live root); writes to the live
|
|
16
|
+
// root by redirection, rm/mv/cp/touch/tee/mkdir/chmod/truncate/dd/sed -i/perl -i.
|
|
17
|
+
// - Edit / Write / MultiEdit / NotebookEdit: file_path / notebook_path under a live root.
|
|
18
|
+
// - Codex apply_patch: "*** Update/Add/Delete File:" paths in the patch text.
|
|
19
|
+
// Always allowed: read-only git (status, log, diff, show, fetch, ls-remote,
|
|
20
|
+
// branch --list, ...), reads, and anything under a `.crewly/` directory (agent
|
|
21
|
+
// data: tickets, findings, drafts).
|
|
22
|
+
//
|
|
23
|
+
// Exit: 0 allowed, 2 blocked (stderr is fed back to the agent), 1 could not check
|
|
24
|
+
// (non-blocking, never a silent pass).
|
|
25
|
+
//
|
|
26
|
+
// Coverage limits: an interpreter one-liner (python -c, node -e), a path built at
|
|
27
|
+
// runtime ($VAR other than HOME, globs, base64), a script the agent writes and
|
|
28
|
+
// runs, and `$(...)` inside double quotes are NOT seen. A speed bump, not a boundary.
|
|
29
|
+
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import os from 'node:os';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
|
|
34
|
+
const PREFIX = 'live-checkout-guard:';
|
|
35
|
+
|
|
36
|
+
/** Git subcommands that never change HEAD, the index, the working tree or refs-for-checkout. */
|
|
37
|
+
const READ_ONLY_GIT = new Set([
|
|
38
|
+
'status', 'log', 'diff', 'show', 'fetch', 'ls-remote', 'ls-files', 'ls-tree', 'rev-parse', 'rev-list',
|
|
39
|
+
'describe', 'blame', 'grep', 'cat-file', 'shortlog', 'show-ref', 'show-branch', 'name-rev', 'merge-base',
|
|
40
|
+
'for-each-ref', 'diff-tree', 'diff-files', 'diff-index', 'var', 'whatchanged', 'count-objects',
|
|
41
|
+
'check-ignore', 'help', 'version', 'verify-commit', 'verify-tag', 'range-diff', 'cherry', 'show-index',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const BRANCH_MUTATING = new Set(['-d', '-D', '-m', '-M', '-c', '-C', '--delete', '--move', '--copy', '-u', '--set-upstream-to', '--unset-upstream', '--edit-description', '-f', '--force']);
|
|
45
|
+
const LIST_FLAGS = new Set(['-l', '--list', '-a', '--all', '-r', '--remotes', '--show-current', '--contains', '--no-contains', '--merged', '--no-merged', '--points-at']);
|
|
46
|
+
const WRAPPERS = new Set(['sudo', 'command', 'nohup', 'time', 'exec', 'env', 'nice', 'builtin', 'xargs', 'stdbuf', 'timeout']);
|
|
47
|
+
const SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh']);
|
|
48
|
+
|
|
49
|
+
// ---- helpers ---------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** realpath of the deepest existing ancestor + the not-yet-existing tail. */
|
|
52
|
+
function realish(p) {
|
|
53
|
+
const abs = path.resolve(p);
|
|
54
|
+
let cur = abs;
|
|
55
|
+
const tail = [];
|
|
56
|
+
for (let i = 0; i < 64; i += 1) {
|
|
57
|
+
try {
|
|
58
|
+
const real = fs.realpathSync(cur);
|
|
59
|
+
return path.join(real, ...tail.reverse());
|
|
60
|
+
} catch {
|
|
61
|
+
const parent = path.dirname(cur);
|
|
62
|
+
if (parent === cur) return abs;
|
|
63
|
+
tail.push(path.basename(cur));
|
|
64
|
+
cur = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return abs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Expand ~, $HOME, ${HOME}; return null when another variable/substitution remains. */
|
|
71
|
+
function expand(word) {
|
|
72
|
+
const home = process.env.HOME || os.homedir();
|
|
73
|
+
let w = word;
|
|
74
|
+
if (home) {
|
|
75
|
+
w = w.replace(/\$\{HOME\}|\$HOME(?![A-Za-z0-9_])/g, home);
|
|
76
|
+
if (w === '~') w = home;
|
|
77
|
+
else if (w.startsWith('~/')) w = home + w.slice(1);
|
|
78
|
+
}
|
|
79
|
+
return /[$`]/.test(w) ? null : w;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const roots = process.argv.slice(2).filter(Boolean).map(realish);
|
|
83
|
+
|
|
84
|
+
/** The live root containing p, or null. Paths under a `.crewly` directory are not live. */
|
|
85
|
+
function liveRootOf(p, { allowDataDirs = true } = {}) {
|
|
86
|
+
const r = realish(p);
|
|
87
|
+
for (const root of roots) {
|
|
88
|
+
if (r === root || r.startsWith(root + path.sep)) {
|
|
89
|
+
if (allowDataDirs && r.slice(root.length).split(path.sep).includes('.crewly')) return null;
|
|
90
|
+
return root;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveWord(word, cwd) {
|
|
97
|
+
const w = expand(word);
|
|
98
|
+
if (w === null || w === '') return null;
|
|
99
|
+
return path.resolve(cwd, w);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const DENY_TAIL =
|
|
103
|
+
'This is the live Crewly install. Clone it into your own scratch dir (git clone <repo-url> <scratch>/crewly && ln -s <live>/node_modules <scratch>/crewly/node_modules) and work there. ' +
|
|
104
|
+
'Read-only git (status, log, diff, show, fetch, ls-remote, branch --list) and files under .crewly/ stay allowed here.';
|
|
105
|
+
|
|
106
|
+
function deny(what, root) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
`${PREFIX} BLOCKED — ${what} (live checkout: ${root}).\n` +
|
|
109
|
+
`${PREFIX} ${DENY_TAIL.replace(/<live>/g, root)}\n` +
|
|
110
|
+
'Changing the live branch, HEAD or files makes Crewly refuse to start on its next restart.\n',
|
|
111
|
+
);
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- shell tokenising ------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Split a command line into simple commands: { words, redirs }.
|
|
119
|
+
* Separators: ; & | && || newline ( ) { } $( ` . Quotes and backslashes are honoured.
|
|
120
|
+
*/
|
|
121
|
+
function parseCommands(src) {
|
|
122
|
+
const cmds = [];
|
|
123
|
+
let words = [];
|
|
124
|
+
let redirs = [];
|
|
125
|
+
let cur = '';
|
|
126
|
+
let has = false;
|
|
127
|
+
let pendingRedir = null;
|
|
128
|
+
const pushWord = () => {
|
|
129
|
+
if (!has) return;
|
|
130
|
+
if (pendingRedir) {
|
|
131
|
+
if (!cur.startsWith('&')) redirs.push(cur);
|
|
132
|
+
pendingRedir = null;
|
|
133
|
+
} else {
|
|
134
|
+
words.push(cur);
|
|
135
|
+
}
|
|
136
|
+
cur = '';
|
|
137
|
+
has = false;
|
|
138
|
+
};
|
|
139
|
+
const endCmd = () => {
|
|
140
|
+
pushWord();
|
|
141
|
+
pendingRedir = null;
|
|
142
|
+
if (words.length || redirs.length) cmds.push({ words, redirs });
|
|
143
|
+
words = [];
|
|
144
|
+
redirs = [];
|
|
145
|
+
};
|
|
146
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
147
|
+
const c = src[i];
|
|
148
|
+
if (c === "'") {
|
|
149
|
+
const j = src.indexOf("'", i + 1);
|
|
150
|
+
const end = j === -1 ? src.length : j;
|
|
151
|
+
cur += src.slice(i + 1, end);
|
|
152
|
+
has = true;
|
|
153
|
+
i = end;
|
|
154
|
+
} else if (c === '"') {
|
|
155
|
+
let j = i + 1;
|
|
156
|
+
let s = '';
|
|
157
|
+
while (j < src.length && src[j] !== '"') {
|
|
158
|
+
if (src[j] === '\\' && j + 1 < src.length && '"\\$`'.includes(src[j + 1])) {
|
|
159
|
+
s += src[j + 1];
|
|
160
|
+
j += 2;
|
|
161
|
+
} else {
|
|
162
|
+
s += src[j];
|
|
163
|
+
j += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
cur += s;
|
|
167
|
+
has = true;
|
|
168
|
+
i = j;
|
|
169
|
+
} else if (c === '\\') {
|
|
170
|
+
if (i + 1 < src.length) {
|
|
171
|
+
if (src[i + 1] !== '\n') {
|
|
172
|
+
cur += src[i + 1];
|
|
173
|
+
has = true;
|
|
174
|
+
}
|
|
175
|
+
i += 1;
|
|
176
|
+
}
|
|
177
|
+
} else if (c === ';' || c === '\n' || c === '(' || c === ')' || c === '`' || c === '{' && !has && /\s/.test(src[i + 1] || ' ') || c === '}' && !has) {
|
|
178
|
+
endCmd();
|
|
179
|
+
} else if (c === '$' && src[i + 1] === '(') {
|
|
180
|
+
endCmd();
|
|
181
|
+
i += 1;
|
|
182
|
+
} else if (c === '&' || c === '|') {
|
|
183
|
+
// &> and >& and >| belong to redirections
|
|
184
|
+
if (c === '&' && src[i + 1] === '>') {
|
|
185
|
+
pushWord();
|
|
186
|
+
pendingRedir = '>';
|
|
187
|
+
i += src[i + 2] === '>' ? 2 : 1;
|
|
188
|
+
} else {
|
|
189
|
+
endCmd();
|
|
190
|
+
if (src[i + 1] === c) i += 1;
|
|
191
|
+
}
|
|
192
|
+
} else if (c === '>') {
|
|
193
|
+
// fd prefix like 2> was already read into cur as digits
|
|
194
|
+
if (has && /^\d+$/.test(cur)) {
|
|
195
|
+
cur = '';
|
|
196
|
+
has = false;
|
|
197
|
+
} else {
|
|
198
|
+
pushWord();
|
|
199
|
+
}
|
|
200
|
+
i += src[i + 1] === '>' || src[i + 1] === '|' ? 1 : 0;
|
|
201
|
+
pendingRedir = '>';
|
|
202
|
+
} else if (c === '<') {
|
|
203
|
+
if (has && /^\d+$/.test(cur)) {
|
|
204
|
+
cur = '';
|
|
205
|
+
has = false;
|
|
206
|
+
} else {
|
|
207
|
+
pushWord();
|
|
208
|
+
}
|
|
209
|
+
if (src[i + 1] === '<') i += 1; // heredoc marker: next word is the delimiter, not a path
|
|
210
|
+
pendingRedir = '<';
|
|
211
|
+
} else if (/\s/.test(c)) {
|
|
212
|
+
if (pendingRedir === '<' && has) {
|
|
213
|
+
cur = '';
|
|
214
|
+
has = false;
|
|
215
|
+
pendingRedir = null;
|
|
216
|
+
} else {
|
|
217
|
+
pushWord();
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
cur += c;
|
|
221
|
+
has = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
endCmd();
|
|
225
|
+
return cmds;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- analysis --------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
function stripWrappers(words) {
|
|
231
|
+
let i = 0;
|
|
232
|
+
const env = {};
|
|
233
|
+
for (;;) {
|
|
234
|
+
const w = words[i];
|
|
235
|
+
if (w === undefined) break;
|
|
236
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(w);
|
|
237
|
+
if (m) {
|
|
238
|
+
env[m[1]] = m[2];
|
|
239
|
+
i += 1;
|
|
240
|
+
} else if (WRAPPERS.has(path.basename(w))) {
|
|
241
|
+
const wrapper = path.basename(w);
|
|
242
|
+
i += 1;
|
|
243
|
+
while (words[i] !== undefined && words[i].startsWith('-')) {
|
|
244
|
+
const opt = words[i];
|
|
245
|
+
i += 1;
|
|
246
|
+
if (wrapper === 'nice' && opt === '-n') i += 1;
|
|
247
|
+
}
|
|
248
|
+
if (wrapper === 'timeout') i += 1; // duration
|
|
249
|
+
} else break;
|
|
250
|
+
}
|
|
251
|
+
return { env, words: words.slice(i) };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function gitSubcommand(args, state) {
|
|
255
|
+
let i = 0;
|
|
256
|
+
let dir = state.cwd;
|
|
257
|
+
let target = null; // explicit --git-dir / --work-tree
|
|
258
|
+
while (i < args.length && args[i].startsWith('-')) {
|
|
259
|
+
const a = args[i];
|
|
260
|
+
if (a === '-C') {
|
|
261
|
+
const p = resolveWord(args[i + 1] ?? '', dir);
|
|
262
|
+
if (p) dir = p;
|
|
263
|
+
i += 2;
|
|
264
|
+
} else if (a === '-c' || a === '--namespace' || a === '--exec-path') i += 2;
|
|
265
|
+
else if (a === '--git-dir' || a === '--work-tree') {
|
|
266
|
+
target = resolveWord(args[i + 1] ?? '', dir) ?? target;
|
|
267
|
+
i += 2;
|
|
268
|
+
} else if (a.startsWith('--git-dir=') || a.startsWith('--work-tree=')) {
|
|
269
|
+
target = resolveWord(a.slice(a.indexOf('=') + 1), dir) ?? target;
|
|
270
|
+
i += 1;
|
|
271
|
+
} else i += 1;
|
|
272
|
+
}
|
|
273
|
+
return { sub: args[i] ?? '', rest: args.slice(i + 1), dir, target };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function hasAny(args, set) {
|
|
277
|
+
return args.some((a) => set.has(a) || [...set].some((s) => s.startsWith('--') && a.startsWith(s + '=')));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Whether `git <sub> <rest>` is read-only. */
|
|
281
|
+
function gitIsReadOnly(sub, rest) {
|
|
282
|
+
if (READ_ONLY_GIT.has(sub)) return true;
|
|
283
|
+
const flags = rest.filter((a) => a.startsWith('-'));
|
|
284
|
+
const positional = rest.filter((a) => !a.startsWith('-'));
|
|
285
|
+
switch (sub) {
|
|
286
|
+
case 'branch':
|
|
287
|
+
if (hasAny(rest, BRANCH_MUTATING)) return false;
|
|
288
|
+
return hasAny(rest, LIST_FLAGS) || positional.length === 0;
|
|
289
|
+
case 'tag':
|
|
290
|
+
if (flags.some((f) => ['-d', '-a', '-s', '-f', '-m', '-u', '--delete', '--force', '--annotate', '--sign'].includes(f))) return false;
|
|
291
|
+
return flags.some((f) => ['-l', '--list', '-n', '--contains', '--points-at', '--merged', '--no-merged', '-v', '--verify'].includes(f)) || positional.length === 0;
|
|
292
|
+
case 'remote':
|
|
293
|
+
return positional.length === 0 || ['show', 'get-url'].includes(positional[0]);
|
|
294
|
+
case 'config':
|
|
295
|
+
return flags.some((f) => ['--get', '--get-all', '--get-regexp', '-l', '--list', '--show-origin'].includes(f));
|
|
296
|
+
case 'stash':
|
|
297
|
+
return ['list', 'show'].includes(positional[0] ?? '');
|
|
298
|
+
case 'worktree':
|
|
299
|
+
return positional[0] === 'list';
|
|
300
|
+
case 'submodule':
|
|
301
|
+
return positional.length === 0 || ['status', 'summary'].includes(positional[0]);
|
|
302
|
+
case 'reflog':
|
|
303
|
+
return !positional.some((p) => ['expire', 'delete'].includes(p));
|
|
304
|
+
case 'archive':
|
|
305
|
+
return true;
|
|
306
|
+
default:
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const CLONE_VALUE_OPTS = new Set(['-b', '--branch', '--depth', '-o', '--origin', '--reference', '--template', '-c', '--config', '-j', '--jobs', '--filter', '--separate-git-dir', '--shallow-since', '--shallow-exclude', '-u', '--upload-pack']);
|
|
312
|
+
|
|
313
|
+
function cloneDestination(rest, cwd) {
|
|
314
|
+
const pos = [];
|
|
315
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
316
|
+
if (CLONE_VALUE_OPTS.has(rest[i])) i += 1;
|
|
317
|
+
else if (!rest[i].startsWith('-')) pos.push(rest[i]);
|
|
318
|
+
}
|
|
319
|
+
const dest = pos[1] ?? path.basename(pos[0] ?? '').replace(/\.git$/, '');
|
|
320
|
+
return dest ? resolveWord(dest, cwd) : null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function analyzeSimple(cmd, state, depth) {
|
|
324
|
+
const { env, words } = stripWrappers(cmd.words);
|
|
325
|
+
|
|
326
|
+
// redirection targets
|
|
327
|
+
for (const r of cmd.redirs) {
|
|
328
|
+
const p = resolveWord(r, state.cwd);
|
|
329
|
+
const root = p && liveRootOf(p);
|
|
330
|
+
if (root) deny(`redirection writes into the live checkout (${r})`, root);
|
|
331
|
+
}
|
|
332
|
+
if (words.length === 0) return;
|
|
333
|
+
const verb = path.basename(words[0]);
|
|
334
|
+
const args = words.slice(1);
|
|
335
|
+
|
|
336
|
+
if (SHELLS.has(verb)) {
|
|
337
|
+
const ci = args.findIndex((a) => /^-[a-z]*c$/.test(a));
|
|
338
|
+
if (ci !== -1 && args[ci + 1] !== undefined && depth < 4) analyze(args[ci + 1], state, depth + 1);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (verb === 'cd' || verb === 'pushd') {
|
|
342
|
+
const t = args.find((a) => !a.startsWith('-')) ?? '~';
|
|
343
|
+
const p = resolveWord(t, state.cwd);
|
|
344
|
+
if (p) state.cwd = p;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (verb === 'git') {
|
|
349
|
+
const g = gitSubcommand(args, state);
|
|
350
|
+
const envTarget = env.GIT_DIR || env.GIT_WORK_TREE;
|
|
351
|
+
const explicit = g.target ?? (envTarget ? resolveWord(envTarget, state.cwd) : null);
|
|
352
|
+
if (g.sub === 'clone' || g.sub === 'init') {
|
|
353
|
+
const dest = g.sub === 'clone' ? cloneDestination(g.rest, g.dir) : resolveWord(g.rest.find((a) => !a.startsWith('-')) ?? '.', g.dir);
|
|
354
|
+
const root = dest && liveRootOf(dest);
|
|
355
|
+
if (root) deny(`git ${g.sub} would create a repository inside the live checkout`, root);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const root = liveRootOf(g.dir) || (explicit ? liveRootOf(explicit) : null);
|
|
359
|
+
if (!root) return;
|
|
360
|
+
if (gitIsReadOnly(g.sub, g.rest)) return;
|
|
361
|
+
deny(`\`git ${g.sub || '(no subcommand)'}\` would change the live Crewly checkout`, root);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// file-mutating verbs
|
|
366
|
+
const fileArgs = args.filter((a) => !a.startsWith('-'));
|
|
367
|
+
const check = (list, what) => {
|
|
368
|
+
for (const a of list) {
|
|
369
|
+
const p = resolveWord(a, state.cwd);
|
|
370
|
+
const root = p && liveRootOf(p);
|
|
371
|
+
if (root) deny(`\`${verb}\` ${what} (${a})`, root);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
switch (verb) {
|
|
375
|
+
case 'rm': case 'rmdir': case 'unlink': case 'chmod': case 'chown': case 'chgrp': case 'truncate':
|
|
376
|
+
case 'touch': case 'tee': case 'mv': case 'shred': case 'mkdir':
|
|
377
|
+
check(fileArgs, 'would modify the live checkout');
|
|
378
|
+
break;
|
|
379
|
+
case 'sed': case 'gsed': case 'perl':
|
|
380
|
+
if (args.some((a) => /^-[A-Za-z]*i/.test(a) || a.startsWith('--in-place'))) check(fileArgs, 'edits a file in place in the live checkout');
|
|
381
|
+
break;
|
|
382
|
+
case 'cp': case 'ln': case 'install': case 'rsync': case 'scp': {
|
|
383
|
+
const ti = args.findIndex((a) => a === '-t' || a === '--target-directory');
|
|
384
|
+
const dest = ti !== -1 ? [args[ti + 1]] : fileArgs.slice(-1);
|
|
385
|
+
check(dest.filter(Boolean), 'would write into the live checkout');
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
case 'dd':
|
|
389
|
+
check(args.filter((a) => a.startsWith('of=')).map((a) => a.slice(3)), 'writes into the live checkout');
|
|
390
|
+
break;
|
|
391
|
+
default:
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function analyze(command, state, depth = 0) {
|
|
396
|
+
for (const cmd of parseCommands(command)) analyzeSimple(cmd, state, depth);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Paths a Codex apply_patch text touches. */
|
|
400
|
+
function patchPaths(text) {
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const m of text.matchAll(/^\*\*\* (?:Update|Add|Delete) File:\s*(.+?)\s*$/gm)) out.push(m[1]);
|
|
403
|
+
for (const m of text.matchAll(/^\*\*\* Move to:\s*(.+?)\s*$/gm)) out.push(m[1]);
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ---- main ------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
let raw = '';
|
|
410
|
+
process.stdin.setEncoding('utf8');
|
|
411
|
+
process.stdin.on('data', (d) => { raw += d; });
|
|
412
|
+
process.stdin.on('end', () => {
|
|
413
|
+
if (roots.length === 0) {
|
|
414
|
+
process.stderr.write(`${PREFIX} NO LIVE ROOT GIVEN — nothing checked; refusing to report this call as safe\n`);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
let input;
|
|
418
|
+
try {
|
|
419
|
+
input = JSON.parse(raw);
|
|
420
|
+
} catch {
|
|
421
|
+
process.stderr.write(`${PREFIX} unparsable hook input — nothing checked\n`);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
const tool = String(input.tool_name ?? '');
|
|
425
|
+
const ti = input.tool_input && typeof input.tool_input === 'object' ? input.tool_input : {};
|
|
426
|
+
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
|
|
427
|
+
|
|
428
|
+
const fileTargets = [];
|
|
429
|
+
for (const k of ['file_path', 'notebook_path', 'path', 'filePath']) if (typeof ti[k] === 'string') fileTargets.push(ti[k]);
|
|
430
|
+
const cmdText = typeof ti.command === 'string' ? ti.command : typeof ti.cmd === 'string' ? ti.cmd : '';
|
|
431
|
+
const isPatch = tool === 'apply_patch' || cmdText.includes('*** Begin Patch') || (typeof ti.input === 'string' && ti.input.includes('*** Begin Patch'));
|
|
432
|
+
|
|
433
|
+
if (isPatch) {
|
|
434
|
+
const text = cmdText || String(ti.input ?? '');
|
|
435
|
+
for (const f of patchPaths(text)) {
|
|
436
|
+
const p = resolveWord(f, cwd);
|
|
437
|
+
const root = p && liveRootOf(p);
|
|
438
|
+
if (root) deny(`apply_patch edits a file in the live checkout (${f})`, root);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (/^(Edit|Write|MultiEdit|NotebookEdit|str_replace_editor)$/.test(tool) || (!cmdText && fileTargets.length && /edit|write|patch/i.test(tool))) {
|
|
442
|
+
for (const f of fileTargets) {
|
|
443
|
+
const p = resolveWord(f, cwd);
|
|
444
|
+
const root = p && liveRootOf(p);
|
|
445
|
+
if (root) deny(`${tool} would edit a file in the live checkout (${f})`, root);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (cmdText && !isPatch) analyze(cmdText, { cwd });
|
|
449
|
+
|
|
450
|
+
process.stdout.write(`${PREFIX} allowed — ${roots.length} live root(s) checked\n`);
|
|
451
|
+
process.exit(0);
|
|
452
|
+
});
|
|
@@ -102,7 +102,8 @@ tells the system that you are the one replying.
|
|
|
102
102
|
| `--to <messageId>` | the message you answer (from your prompt) — goes to its conversation and thread |
|
|
103
103
|
| `--work-item <id>` | the work item you answer about — goes where that work came from |
|
|
104
104
|
| `--decision <id>` | the owner decision you follow up (`D-12`) — goes to the card's thread |
|
|
105
|
-
| `--drive <session
|
|
105
|
+
| `--drive <session> --ack "<text>"` | Drive mode, FIRST, within seconds of the message: a short acknowledgement or the direct answer ("On it — about five minutes.") |
|
|
106
|
+
| `--drive <session>` | the owner is in Drive mode (listening on the phone, your prompt says `[Drive mode · session …]`): the full result goes to their phone, not Slack — conclusion first, at most 3 spoken sentences; a decision → 2–3 options; no URLs, tables or code; details go in your end-of-session recap |
|
|
106
107
|
| `--drive <session> --recap "<text>"` | after the Drive mode session ended: your ONE recap ("Drive mode recap — you said …; I did …; next: …"), posted in your DM with the owner (or the channel); it closes the conversation |
|
|
107
108
|
| `--conversation <id>` / `--thread <key>` | hints only: used when they name a conversation you are in (a thread key must be that conversation's channel); otherwise ignored |
|
|
108
109
|
|
|
@@ -20,7 +20,8 @@ Usage:
|
|
|
20
20
|
bash execute.sh --new-thread "Wiki link audit" "Found 3 broken links: …" # a new topic
|
|
21
21
|
bash execute.sh --ticket TKT-187 "Here is the preview: https://…" # answer about a ticket
|
|
22
22
|
bash execute.sh --to <messageId> "Yes — done." # answer a specific message
|
|
23
|
-
bash execute.sh --drive <session> "
|
|
23
|
+
bash execute.sh --drive <session> --ack "On it — about five minutes." # Drive mode: acknowledge within seconds
|
|
24
|
+
bash execute.sh --drive <session> "Done: published to 1,200 readers." # Drive mode: the full result (voice)
|
|
24
25
|
bash execute.sh --drive <session> --recap "Drive mode recap — …" # after the Drive mode session ended
|
|
25
26
|
|
|
26
27
|
Options:
|
|
@@ -36,7 +37,10 @@ Options:
|
|
|
36
37
|
--work-item The work item you are answering about
|
|
37
38
|
--decision The owner decision you are following up (D-12)
|
|
38
39
|
--drive <session> The owner is in Drive mode (listening on the phone): your answer
|
|
39
|
-
goes to their phone, not Slack.
|
|
40
|
+
goes to their phone, not Slack. Conclusion first, at most 3
|
|
41
|
+
spoken sentences; a decision → 2–3 options; no URLs/tables.
|
|
42
|
+
--ack With --drive: a quick acknowledgement or direct answer within
|
|
43
|
+
seconds ("On it — about five minutes."); the full result follows
|
|
40
44
|
--recap With --drive, after the session ended: your one recap, posted
|
|
41
45
|
in your DM with the owner (or the channel); closes the conversation
|
|
42
46
|
--conversation|-C Only if your prompt tells you to answer somewhere specific
|
|
@@ -58,6 +62,7 @@ WORK_ITEM=""
|
|
|
58
62
|
DECISION=""
|
|
59
63
|
DRIVE=""
|
|
60
64
|
RECAP=""
|
|
65
|
+
ACK=""
|
|
61
66
|
|
|
62
67
|
while [[ $# -gt 0 ]]; do
|
|
63
68
|
case "$1" in
|
|
@@ -76,6 +81,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
76
81
|
--decision) DECISION="$2"; shift 2 ;;
|
|
77
82
|
--drive) DRIVE="$2"; shift 2 ;;
|
|
78
83
|
--recap) RECAP="1"; shift ;;
|
|
84
|
+
--ack) ACK="1"; shift ;;
|
|
79
85
|
--help|-h) print_usage; exit 0 ;;
|
|
80
86
|
--) shift; if [[ $# -gt 0 && -z "$TEXT" ]]; then TEXT="$*"; fi; break ;;
|
|
81
87
|
-*) echo "{\"success\":false,\"error\":\"Unknown option: $1\"}" >&2; print_usage >&2; exit 2 ;;
|
|
@@ -95,7 +101,7 @@ fi
|
|
|
95
101
|
# Literal \n (from JSON-escaped text) → real newlines
|
|
96
102
|
if [ -n "$TEXT" ]; then _NL=$'\n'; TEXT="${TEXT//\\n/$_NL}"; fi
|
|
97
103
|
|
|
98
|
-
BODY=$(TEXT="$TEXT" INTERIM="$INTERIM" NONE="$NONE" ADDS_NEW="$ADDS_NEW" CONVERSATION_ID="$CONVERSATION_ID" THREAD="$THREAD" NEW_THREAD="$NEW_THREAD" TICKET="$TICKET" TO="$TO" WORK_ITEM="$WORK_ITEM" DECISION="$DECISION" DRIVE="$DRIVE" RECAP="$RECAP" python3 -c '
|
|
104
|
+
BODY=$(TEXT="$TEXT" INTERIM="$INTERIM" NONE="$NONE" ADDS_NEW="$ADDS_NEW" CONVERSATION_ID="$CONVERSATION_ID" THREAD="$THREAD" NEW_THREAD="$NEW_THREAD" TICKET="$TICKET" TO="$TO" WORK_ITEM="$WORK_ITEM" DECISION="$DECISION" DRIVE="$DRIVE" RECAP="$RECAP" ACK="$ACK" python3 -c '
|
|
99
105
|
import os, json
|
|
100
106
|
p = {}
|
|
101
107
|
if os.environ.get("NONE"):
|
|
@@ -124,6 +130,8 @@ if os.environ.get("DRIVE"):
|
|
|
124
130
|
p["drive"] = os.environ["DRIVE"]
|
|
125
131
|
if os.environ.get("RECAP"):
|
|
126
132
|
p["recap"] = True
|
|
133
|
+
if os.environ.get("ACK"):
|
|
134
|
+
p["ack"] = True
|
|
127
135
|
print(json.dumps(p))
|
|
128
136
|
')
|
|
129
137
|
|
|
@@ -92,6 +92,10 @@ run_skill --drive drv_abc12345 --recap "Drive mode recap — you said X; next: n
|
|
|
92
92
|
LOG="$(cat "$STUB_LOG")"
|
|
93
93
|
check "drive session" "$LOG" 'drive\\": \\"drv_abc12345'
|
|
94
94
|
check "recap flag" "$LOG" 'recap\\": true'
|
|
95
|
+
: > "$STUB_LOG"
|
|
96
|
+
run_skill --drive drv_abc12345 --ack "On it" </dev/null >/dev/null 2>&1 || true
|
|
97
|
+
LOG="$(cat "$STUB_LOG")"
|
|
98
|
+
check "ack flag" "$LOG" 'ack\\": true'
|
|
95
99
|
|
|
96
100
|
echo "test 4c: references (--ticket / --to / --work-item / --decision)"
|
|
97
101
|
: > "$STUB_LOG"
|
|
@@ -1016,12 +1016,29 @@ export declare const CONTROL_PLANE_GUARD_CONSTANTS: {
|
|
|
1016
1016
|
/** Files under CREWLY_HOME that agents must not read with the built-in tools either. */
|
|
1017
1017
|
readonly CREWLY_HOME_READ_DENIED_FILES: readonly ["api-token"];
|
|
1018
1018
|
/** Write-protected directories under the install root (whole subtree). */
|
|
1019
|
-
readonly INSTALL_DIRS: readonly ["config/skills/orchestrator/stop-agent", "config/skills/orchestrator/start-agent", "config/skills/orchestrator/terminate-agent", "config/skills/orchestrator/stop-team", "config/skills/orchestrator/start-team", "config/skills/orchestrator/restart-crewly", "config/hooks/control-plane-guard", "config/hooks/agent-status", "config/hooks/subagent-guard", "config/hooks/credential-guard", "dist"];
|
|
1019
|
+
readonly INSTALL_DIRS: readonly ["config/skills/orchestrator/stop-agent", "config/skills/orchestrator/start-agent", "config/skills/orchestrator/terminate-agent", "config/skills/orchestrator/stop-team", "config/skills/orchestrator/start-team", "config/skills/orchestrator/restart-crewly", "config/hooks/control-plane-guard", "config/hooks/agent-status", "config/hooks/subagent-guard", "config/hooks/credential-guard", "config/hooks/live-checkout-guard", "dist"];
|
|
1020
1020
|
/** Write-protected files under the install root. */
|
|
1021
1021
|
readonly INSTALL_FILES: readonly ["config/skills/_common/lib.sh"];
|
|
1022
1022
|
/** Write-protected directories under the agent's project path (whole subtree). */
|
|
1023
1023
|
readonly PROJECT_DIRS: readonly [".claude/agents", ".crewly/triggers"];
|
|
1024
1024
|
};
|
|
1025
|
+
/**
|
|
1026
|
+
* Live-checkout guard: agents may not change the checkout the running Crewly
|
|
1027
|
+
* is built from (git state changes or file edits). Incident 2026-10-08/09: an
|
|
1028
|
+
* agent switched branches in it and the next restart refused to start.
|
|
1029
|
+
*/
|
|
1030
|
+
export declare const LIVE_CHECKOUT_GUARD_CONSTANTS: {
|
|
1031
|
+
/** Backend env var; `0` turns the guard off for sessions launched by this backend. */
|
|
1032
|
+
readonly KILL_SWITCH_ENV: "CREWLY_LIVE_CHECKOUT_GUARD";
|
|
1033
|
+
/** Value of KILL_SWITCH_ENV that disables the guard. */
|
|
1034
|
+
readonly KILL_SWITCH_OFF_VALUE: "0";
|
|
1035
|
+
/** The hook script (node), relative to the install root. */
|
|
1036
|
+
readonly HOOK_SCRIPT: "config/hooks/live-checkout-guard/guard.mjs";
|
|
1037
|
+
/** Claude Code tools the hook is attached to. */
|
|
1038
|
+
readonly CLAUDE_MATCHER: "Bash|Edit|Write|MultiEdit|NotebookEdit";
|
|
1039
|
+
/** Codex tools the hook is attached to (shell, plus its patch tool). */
|
|
1040
|
+
readonly CODEX_MATCHER: "Bash|apply_patch|Edit|Write";
|
|
1041
|
+
};
|
|
1025
1042
|
/**
|
|
1026
1043
|
* Credential guard (specs/2026-10-04-agent-credential-isolation.md, layer 2):
|
|
1027
1044
|
* a pre-tool hook, on every runtime that has one, that refuses an agent's
|
|
@@ -7357,6 +7374,18 @@ export declare const REMOTE_MCP_CONSTANTS: {
|
|
|
7357
7374
|
readonly MAX_TOOL_NAMES: 50;
|
|
7358
7375
|
/** Catalog presets the add form offers. */
|
|
7359
7376
|
readonly PROVIDERS: readonly string[];
|
|
7377
|
+
/**
|
|
7378
|
+
* Tools an AGENT may never call through the proxy, per server kind (the
|
|
7379
|
+
* server's provider, or its id when it has none). Compared after
|
|
7380
|
+
* trim + NFKC + lower-casing, so case and width tricks do not bypass it.
|
|
7381
|
+
* Zoho has no draft tool: a draft is a mode of sendEmail, so one missing
|
|
7382
|
+
* field sends mail. Mail is only ever drafted through the backend's
|
|
7383
|
+
* draft-only path (CREW-400). resendVerification* mails an address a
|
|
7384
|
+
* verification link; denied too since an agent has no reason to trigger it.
|
|
7385
|
+
*/
|
|
7386
|
+
readonly DENIED_AGENT_TOOLS: Record<string, readonly string[]>;
|
|
7387
|
+
/** Message an agent gets when it calls a denied tool. */
|
|
7388
|
+
readonly DENIED_TOOL_MESSAGE: "sending mail is not allowed for agents; save a draft instead";
|
|
7360
7389
|
/**
|
|
7361
7390
|
* OAuth state (clients, tokens, pending sign-ins) under CREWLY_HOME:
|
|
7362
7391
|
* AES-256-GCM with the install's master key, mode 0600.
|
|
@@ -7558,6 +7587,57 @@ export declare const DRIVE_CONSTANTS: {
|
|
|
7558
7587
|
readonly FETCH_RETRIES: 3;
|
|
7559
7588
|
/** Metadata `via` on every chat-v2 row Drive mode writes (the Slack mirrors skip them) */
|
|
7560
7589
|
readonly VIA: "drive-mode";
|
|
7590
|
+
/** Keep-warm (v3): agents one session keeps warm, at most */
|
|
7591
|
+
readonly MAX_WARM: 20;
|
|
7592
|
+
};
|
|
7593
|
+
/**
|
|
7594
|
+
* Drive mode v3 status briefing (specs/2026-10-09-drive-mode-v3.md §1): the
|
|
7595
|
+
* compact per-team / per-agent snapshot this machine keeps up to date and
|
|
7596
|
+
* pushes to Crewly Cloud, so the voice answers status questions with no
|
|
7597
|
+
* agent round trip. Built from structured data only — no LLM calls.
|
|
7598
|
+
*/
|
|
7599
|
+
export declare const DRIVE_BRIEFING_CONSTANTS: {
|
|
7600
|
+
/** Cloud route (`:instanceId` = this machine's device id) */
|
|
7601
|
+
readonly PUT_PATH: "/api/cloud/instances/:instanceId/briefing";
|
|
7602
|
+
/** `0` / `off` turns the snapshot push off */
|
|
7603
|
+
readonly KILL_SWITCH_ENV: "CREWLY_DRIVE_BRIEFING";
|
|
7604
|
+
/** Gather events this long before rebuilding (ms) */
|
|
7605
|
+
readonly DEBOUNCE_MS: 3000;
|
|
7606
|
+
/** At most one rebuild this often; events in between are coalesced (ms) */
|
|
7607
|
+
readonly MIN_INTERVAL_MS: 30000;
|
|
7608
|
+
/** A project's tickets are re-read after this even without a change event (ms) */
|
|
7609
|
+
readonly TICKET_CACHE_MS: number;
|
|
7610
|
+
/** The task pool is re-read after this even without an event (ms) */
|
|
7611
|
+
readonly POOL_CACHE_MS: 60000;
|
|
7612
|
+
/** Rebuild anyway this often; uploads only when it changed (ms) */
|
|
7613
|
+
readonly CHECK_INTERVAL_MS: 60000;
|
|
7614
|
+
/** Upload even when unchanged this often (Cloud sees the machine is alive) (ms) */
|
|
7615
|
+
readonly FULL_SYNC_INTERVAL_MS: number;
|
|
7616
|
+
/** Cloud call timeout (ms) */
|
|
7617
|
+
readonly REQUEST_TIMEOUT_MS: 10000;
|
|
7618
|
+
/** Backoff after a failed upload (ms) */
|
|
7619
|
+
readonly BACKOFF_INITIAL_MS: 15000;
|
|
7620
|
+
readonly BACKOFF_MAX_MS: number;
|
|
7621
|
+
/** Cloud does not take snapshots (not deployed / refused): check again after (ms) */
|
|
7622
|
+
readonly UNAVAILABLE_RETRY_MS: number;
|
|
7623
|
+
/** Largest snapshot sent (bytes of JSON); Cloud refuses above 64 KB */
|
|
7624
|
+
readonly MAX_BYTES: number;
|
|
7625
|
+
readonly MAX_TEAMS: 30;
|
|
7626
|
+
readonly MAX_AGENTS: 80;
|
|
7627
|
+
readonly MAX_ITEMS: 80;
|
|
7628
|
+
readonly MAX_WAITING: 30;
|
|
7629
|
+
/** An agent's recent messages to the owner, at most */
|
|
7630
|
+
readonly MAX_LAST_TO_OWNER: 3;
|
|
7631
|
+
/** How far back an agent's messages to the owner are read (ms) */
|
|
7632
|
+
readonly OWNER_FEED_WINDOW_MS: number;
|
|
7633
|
+
/** chat-v2 rows scanned for them, at most */
|
|
7634
|
+
readonly OWNER_FEED_SCAN_LIMIT: 1500;
|
|
7635
|
+
/** Done / cancelled work older than this is left out (ms) */
|
|
7636
|
+
readonly DONE_WINDOW_MS: number;
|
|
7637
|
+
/** Text lengths (characters) */
|
|
7638
|
+
readonly NAME_MAX: 60;
|
|
7639
|
+
readonly TITLE_MAX: 140;
|
|
7640
|
+
readonly TEXT_MAX: 240;
|
|
7561
7641
|
};
|
|
7562
7642
|
/**
|
|
7563
7643
|
* Drive mode voice layer: Gemini Live ephemeral tokens minted on this
|