@superjackfruitlabs/salaam 0.0.1 → 0.0.2

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +25 -17
  3. package/dist/src/a2a-backend.js +225 -0
  4. package/dist/src/a2a-inbound.js +487 -0
  5. package/dist/src/a2a-peer.js +370 -0
  6. package/dist/src/a2a-protocol.js +250 -0
  7. package/dist/src/agent-profiles.js +289 -0
  8. package/dist/src/children.js +132 -26
  9. package/dist/src/claude-provider.js +2 -179
  10. package/dist/src/cli-help.js +107 -6
  11. package/dist/src/cli.js +477 -13
  12. package/dist/src/config.js +26 -3
  13. package/dist/src/credentials.js +7 -2
  14. package/dist/src/external-runtime.js +1 -0
  15. package/dist/src/external-work.js +583 -0
  16. package/dist/src/harness-backend.js +466 -0
  17. package/dist/src/harness-container.js +450 -0
  18. package/dist/src/harness-model-gateway.js +104 -0
  19. package/dist/src/harness-prepare.js +88 -0
  20. package/dist/src/harness-probe.js +111 -0
  21. package/dist/src/harness-protocol.js +325 -0
  22. package/dist/src/harness-readiness.js +50 -0
  23. package/dist/src/harness-rpc.js +206 -0
  24. package/dist/src/harness-tool-identity.js +87 -0
  25. package/dist/src/harness-wire-bridge.js +387 -0
  26. package/dist/src/host.js +6 -1
  27. package/dist/src/house-extension.js +147 -64
  28. package/dist/src/live-provider.js +301 -0
  29. package/dist/src/managed.js +33 -3
  30. package/dist/src/protocol.js +9 -5
  31. package/dist/src/provider-auth.js +300 -0
  32. package/dist/src/provider-config.js +65 -17
  33. package/dist/src/provider-model.js +78 -0
  34. package/dist/src/provider-payload.js +264 -0
  35. package/dist/src/provider-transport.js +36 -0
  36. package/dist/src/provider-usage.js +59 -0
  37. package/dist/src/repository-snapshots.js +219 -0
  38. package/dist/src/service.js +274 -9
  39. package/dist/src/store.js +44 -2
  40. package/dist/src/supervisor.js +246 -35
  41. package/dist/src/task-budget.js +17 -8
  42. package/dist/src/task-context.js +15 -0
  43. package/dist/src/task-driver.js +38 -14
  44. package/dist/src/task-policy.js +20 -1
  45. package/dist/src/task-verification.js +3 -0
  46. package/dist/src/task-work.js +31 -0
  47. package/dist/src/team-work.js +378 -0
  48. package/dist/src/tui-view.js +65 -10
  49. package/dist/src/tui.js +4 -2
  50. package/dist/src/usage.js +18 -0
  51. package/dist/src/worker-boundary.js +298 -0
  52. package/dist/src/worker-workspaces.js +274 -0
  53. package/dist/src/workspaces.js +6 -2
  54. package/harness-runtime/codex-pins.json +10 -0
  55. package/harness-runtime/package-lock.json +560 -0
  56. package/harness-runtime/package.json +9 -0
  57. package/npm-shrinkwrap.json +57 -2
  58. package/package.json +2 -1
  59. package/src/a2a-backend.ts +254 -0
  60. package/src/a2a-inbound.ts +585 -0
  61. package/src/a2a-peer.ts +441 -0
  62. package/src/a2a-protocol.ts +286 -0
  63. package/src/agent-profiles.ts +379 -0
  64. package/src/children.ts +168 -27
  65. package/src/claude-provider.ts +2 -189
  66. package/src/cli-help.ts +107 -6
  67. package/src/cli.ts +602 -12
  68. package/src/config.ts +34 -3
  69. package/src/credentials.ts +9 -2
  70. package/src/external-runtime.ts +54 -0
  71. package/src/external-work.ts +663 -0
  72. package/src/harness-backend.ts +557 -0
  73. package/src/harness-container.ts +543 -0
  74. package/src/harness-model-gateway.ts +138 -0
  75. package/src/harness-prepare.ts +113 -0
  76. package/src/harness-probe.ts +150 -0
  77. package/src/harness-protocol.ts +388 -0
  78. package/src/harness-readiness.ts +71 -0
  79. package/src/harness-rpc.ts +244 -0
  80. package/src/harness-tool-identity.ts +91 -0
  81. package/src/harness-wire-bridge.ts +418 -0
  82. package/src/host.ts +11 -6
  83. package/src/house-extension.ts +165 -66
  84. package/src/live-provider.ts +347 -0
  85. package/src/managed.ts +43 -4
  86. package/src/protocol.ts +36 -8
  87. package/src/provider-auth.ts +336 -0
  88. package/src/provider-config.ts +89 -18
  89. package/src/provider-model.ts +86 -0
  90. package/src/provider-payload.ts +263 -0
  91. package/src/provider-transport.ts +51 -0
  92. package/src/provider-usage.ts +64 -0
  93. package/src/repository-snapshots.ts +259 -0
  94. package/src/service.ts +446 -11
  95. package/src/store.ts +60 -4
  96. package/src/supervisor.ts +294 -41
  97. package/src/task-budget.ts +24 -11
  98. package/src/task-context.ts +17 -0
  99. package/src/task-driver.ts +51 -15
  100. package/src/task-policy.ts +23 -3
  101. package/src/task-verification.ts +2 -0
  102. package/src/task-work.ts +46 -1
  103. package/src/team-work.ts +554 -0
  104. package/src/tui-client.ts +5 -0
  105. package/src/tui-view.ts +164 -12
  106. package/src/tui.ts +5 -2
  107. package/src/usage.ts +17 -0
  108. package/src/worker-boundary.ts +395 -0
  109. package/src/worker-workspaces.ts +390 -0
  110. package/src/workspaces.ts +4 -2
@@ -0,0 +1,71 @@
1
+ import {
2
+ mkdtempSync,
3
+ mkdirSync,
4
+ writeFileSync,
5
+ readFileSync,
6
+ existsSync,
7
+ realpathSync,
8
+ rmSync,
9
+ } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { randomUUID } from 'node:crypto';
12
+ import { DockerHarnessBoundary } from './harness-container.js';
13
+ import { HarnessRpc } from './harness-rpc.js';
14
+
15
+ /** Real readiness, not an advertised upstream capability. Runs without credentials or model traffic. */
16
+ export async function proveHarnessReadiness(
17
+ boundary: DockerHarnessBoundary,
18
+ root: string,
19
+ ): Promise<void> {
20
+ const dir = mkdtempSync(join(realpathSync(root), 'harness-proof-'));
21
+ for (const p of ['work', 'home', 'runtime']) mkdirSync(join(dir, p), { mode: 0o700 });
22
+ writeFileSync(join(dir, 'host-secret'), 'not-mounted');
23
+ writeFileSync(join(dir, 'runtime/readonly'), 'fixed');
24
+ const run = randomUUID();
25
+ let rpc: HarnessRpc | undefined;
26
+ try {
27
+ const script = `(async()=>{const fs=require('node:fs'),net=require('node:net'),cp=require('node:child_process'),rl=require('node:readline');for await(const line of rl.createInterface({input:process.stdin})){const m=JSON.parse(line);const denied=p=>{try{fs.readFileSync(p);return false}catch{return true}};const writeDenied=p=>{try{fs.writeFileSync(p,'bad');return false}catch{return true}};const connect=(host,port)=>new Promise(r=>{const s=net.connect({host,port});s.on('connect',()=>{s.destroy();r(true)});s.on('error',()=>r(false));s.setTimeout(500,()=>{s.destroy();r(false)})});fs.writeFileSync('/workspace/owned','ok');const bash=cp.execFileSync('/bin/bash',['-c','printf bash-ok'],{encoding:'utf8'});const response=await fetch('http://127.0.0.1:47123/proof',{method:'POST',body:'proof'});const result={host:denied(${JSON.stringify(join(dir, 'host-secret'))}),socket:denied('/var/run/docker.sock'),runtime:writeDenied('/runtime/readonly'),root:writeDenied('/escape'),bash,interfaces:fs.readdirSync('/sys/class/net'),external:await connect('1.1.1.1',53),metadata:await connect('169.254.169.254',80),other_port:await connect('127.0.0.1',47124),gateway:await response.text(),status:fs.readFileSync('/proc/self/status','utf8')};cp.spawn('/usr/local/bin/node',['-e',"setTimeout(()=>require('node:fs').writeFileSync('/workspace/late','bad'),1500)"],{detached:true,stdio:'ignore'}).unref();process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\\n')}})().catch(()=>process.exit(2));`;
28
+ const receipt = await boundary.prepare(
29
+ run,
30
+ join(dir, 'work'),
31
+ join(dir, 'runtime'),
32
+ join(dir, 'home'),
33
+ ['/usr/local/bin/node', '-e', script],
34
+ );
35
+ await boundary.gateway(receipt, async (req) => {
36
+ if (req.path !== '/proof' || req.body.toString() !== 'proof')
37
+ throw Error('Unexpected readiness traffic');
38
+ return { status: 200, body: Buffer.from('host-relay') };
39
+ });
40
+ rpc = new HarnessRpc(await boundary.launchHarness(receipt, 'acp'));
41
+ const proof = await rpc.request('proof', {}, 10000);
42
+ if (
43
+ !proof.host ||
44
+ !proof.socket ||
45
+ !proof.runtime ||
46
+ !proof.root ||
47
+ proof.bash !== 'bash-ok' ||
48
+ proof.external ||
49
+ proof.metadata ||
50
+ proof.other_port ||
51
+ JSON.stringify(proof.interfaces) !== '["lo"]' ||
52
+ proof.gateway !== 'host-relay' ||
53
+ !/NoNewPrivs:\s+1/.test(String(proof.status)) ||
54
+ !/Seccomp:\s+2/.test(String(proof.status)) ||
55
+ !/CapEff:\s+0000000000000000/.test(String(proof.status))
56
+ )
57
+ throw Error('Whole-harness readiness proof failed');
58
+ await rpc.close();
59
+ await boundary.cleanupRun(run);
60
+ await new Promise((r) => setTimeout(r, 1700));
61
+ if (
62
+ existsSync(join(dir, 'work/late')) ||
63
+ readFileSync(join(dir, 'work/owned'), 'utf8') !== 'ok'
64
+ )
65
+ throw Error('Harness descendant stop proof failed');
66
+ } finally {
67
+ await rpc?.close();
68
+ await boundary.cleanupRun(run);
69
+ rmSync(dir, { recursive: true, force: true });
70
+ }
71
+ }
@@ -0,0 +1,244 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { TextDecoder } from 'node:util';
3
+ import { ServiceError } from './protocol.js';
4
+
5
+ export type RpcObject = Record<string, unknown>;
6
+ export interface HarnessLaunch {
7
+ executable: string;
8
+ args: string[];
9
+ cwd: string;
10
+ env: Record<string, string>;
11
+ dialect: 'acp' | 'codex';
12
+ }
13
+ export const HARNESS_RPC_LIMITS = {
14
+ frame: 1024 * 1024,
15
+ total: 4 * 1024 * 1024,
16
+ pending: 16,
17
+ timeout: 10000,
18
+ } as const;
19
+ export interface HarnessHandlers {
20
+ request?: (method: string, params: RpcObject) => Promise<unknown>;
21
+ notification?: (method: string, params: RpcObject) => void;
22
+ failed?: (error: Error) => void;
23
+ }
24
+ /** Bounded stdio framing only. A launch is not a workspace or credential sandbox. */
25
+ export class HarnessRpc {
26
+ readonly process: ChildProcessWithoutNullStreams;
27
+ private serial = 0;
28
+ private received = 0;
29
+ private stderr = 0;
30
+ private buffer = '';
31
+ private decoder = new TextDecoder('utf-8', { fatal: true });
32
+ private pending = new Map<
33
+ number,
34
+ { resolve: (value: RpcObject) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }
35
+ >();
36
+ private inbound = new Set<string>();
37
+ private failure?: Error;
38
+ private stopping = false;
39
+ readonly exited: Promise<void>;
40
+ constructor(
41
+ readonly launch: HarnessLaunch,
42
+ readonly handlers: HarnessHandlers = {},
43
+ ) {
44
+ this.process = spawn(launch.executable, launch.args, {
45
+ cwd: launch.cwd,
46
+ env: launch.env,
47
+ stdio: ['pipe', 'pipe', 'pipe'],
48
+ });
49
+ this.exited = new Promise((resolve) =>
50
+ this.process.once('close', () => {
51
+ if (!this.stopping) this.fail(new Error('Harness exited before connection closed'));
52
+ resolve();
53
+ }),
54
+ );
55
+ this.process.on('error', (error) => this.fail(error));
56
+ this.process.stdin.on('error', (error) => this.fail(error));
57
+ this.process.stderr.on('data', (bytes: Buffer) => {
58
+ this.stderr += bytes.length;
59
+ if (this.stderr > HARNESS_RPC_LIMITS.total)
60
+ this.fail(new Error('Harness stderr exceeded its bound'));
61
+ });
62
+ this.process.stdout.on('data', (bytes: Buffer) => {
63
+ try {
64
+ this.received += bytes.length;
65
+ if (this.received > HARNESS_RPC_LIMITS.total)
66
+ throw new Error('Harness output exceeded its bound');
67
+ this.buffer += this.decoder.decode(bytes, { stream: true });
68
+ let end;
69
+ while ((end = this.buffer.indexOf('\n')) >= 0) {
70
+ const line = this.buffer.slice(0, end);
71
+ this.buffer = this.buffer.slice(end + 1);
72
+ if (Buffer.byteLength(line) > HARNESS_RPC_LIMITS.frame)
73
+ throw new Error('Harness frame exceeded its bound');
74
+ if (!line.trim()) throw new Error('Empty harness frame');
75
+ this.message(JSON.parse(line));
76
+ }
77
+ if (Buffer.byteLength(this.buffer) > HARNESS_RPC_LIMITS.frame)
78
+ throw new Error('Harness frame exceeded its bound');
79
+ } catch (error) {
80
+ this.fail(error instanceof Error ? error : new Error(String(error)));
81
+ }
82
+ });
83
+ this.process.stdout.on('end', () => {
84
+ if (
85
+ !this.stopping &&
86
+ (this.buffer.length ||
87
+ (() => {
88
+ try {
89
+ return Boolean(this.decoder.decode());
90
+ } catch {
91
+ return true;
92
+ }
93
+ })())
94
+ )
95
+ this.fail(new Error('Truncated harness frame'));
96
+ });
97
+ }
98
+ private object(value: unknown): RpcObject {
99
+ if (!value || typeof value !== 'object' || Array.isArray(value))
100
+ throw new Error('Invalid harness object');
101
+ return value as RpcObject;
102
+ }
103
+ private message(value: unknown): void {
104
+ if (this.failure || this.stopping) return;
105
+ const message = this.object(value);
106
+ if (this.launch.dialect === 'acp' ? message.jsonrpc !== '2.0' : message.jsonrpc !== undefined)
107
+ throw new Error('Harness RPC dialect mismatch');
108
+ if (typeof message.method === 'string') {
109
+ if (
110
+ message.method.length > 192 ||
111
+ Object.hasOwn(message, 'result') ||
112
+ Object.hasOwn(message, 'error')
113
+ )
114
+ throw new Error('Invalid harness request envelope');
115
+ const params = message.params === undefined ? {} : this.object(message.params);
116
+ if (message.id === undefined) {
117
+ this.handlers.notification?.(message.method, params);
118
+ return;
119
+ }
120
+ if (
121
+ (typeof message.id !== 'string' && typeof message.id !== 'number') ||
122
+ (typeof message.id === 'string' && message.id.length > 256) ||
123
+ (typeof message.id === 'number' && !Number.isSafeInteger(message.id))
124
+ )
125
+ throw new Error('Invalid harness request ID');
126
+ const key = JSON.stringify(message.id);
127
+ if (this.inbound.has(key) || this.inbound.size >= HARNESS_RPC_LIMITS.pending)
128
+ throw new Error('Duplicate or excessive harness requests');
129
+ this.inbound.add(key);
130
+ Promise.resolve()
131
+ .then(() => {
132
+ if (!this.handlers.request)
133
+ throw new ServiceError(
134
+ 'harness_permission_denied',
135
+ 'Harness client authority is unavailable',
136
+ );
137
+ return this.handlers.request(message.method as string, params);
138
+ })
139
+ .then(
140
+ (result) => this.send({ id: message.id, result: result ?? {} }),
141
+ () =>
142
+ this.send({
143
+ id: message.id,
144
+ error: { code: -32000, message: 'Harness request denied' },
145
+ }),
146
+ )
147
+ .catch((error) => this.fail(error))
148
+ .finally(() => this.inbound.delete(key));
149
+ return;
150
+ }
151
+ if (
152
+ !Number.isSafeInteger(message.id) ||
153
+ Object.hasOwn(message, 'result') === Object.hasOwn(message, 'error')
154
+ )
155
+ throw new Error('Invalid harness reply');
156
+ const waiter = this.pending.get(message.id as number);
157
+ if (!waiter) throw new Error('Unowned or duplicate harness reply');
158
+ this.pending.delete(message.id as number);
159
+ clearTimeout(waiter.timer);
160
+ if (Object.hasOwn(message, 'error'))
161
+ waiter.reject(new ServiceError('harness_rpc_error', 'Harness rejected the request'));
162
+ else {
163
+ try {
164
+ waiter.resolve(this.object(message.result));
165
+ } catch (error) {
166
+ waiter.reject(error as Error);
167
+ throw error;
168
+ }
169
+ }
170
+ }
171
+ private send(value: RpcObject): void {
172
+ if (this.failure || this.stopping) throw this.failure ?? new Error('Harness connection closed');
173
+ const frame =
174
+ JSON.stringify({ ...(this.launch.dialect === 'acp' ? { jsonrpc: '2.0' } : {}), ...value }) +
175
+ '\n';
176
+ if (Buffer.byteLength(frame) > HARNESS_RPC_LIMITS.frame)
177
+ throw new Error('Outbound harness frame exceeded its bound');
178
+ if (this.process.stdin.writableLength + Buffer.byteLength(frame) > HARNESS_RPC_LIMITS.total)
179
+ throw new Error('Harness input backpressure exceeded its bound');
180
+ this.process.stdin.write(frame);
181
+ }
182
+ request(
183
+ method: string,
184
+ params: RpcObject,
185
+ timeout: number = HARNESS_RPC_LIMITS.timeout,
186
+ ): Promise<RpcObject> {
187
+ if (this.failure || this.stopping)
188
+ return Promise.reject(this.failure ?? new Error('Harness connection closed'));
189
+ if (
190
+ this.pending.size >= HARNESS_RPC_LIMITS.pending ||
191
+ !Number.isSafeInteger(timeout) ||
192
+ timeout < 1 ||
193
+ timeout > 300000
194
+ )
195
+ return Promise.reject(new Error('Harness request bound exceeded'));
196
+ const id = ++this.serial;
197
+ return new Promise((resolve, reject) => {
198
+ const timer = setTimeout(
199
+ () =>
200
+ this.fail(
201
+ new ServiceError(
202
+ 'harness_outcome_unknown',
203
+ 'Harness reply deadline reached; the request must not be replayed',
204
+ ),
205
+ ),
206
+ timeout,
207
+ );
208
+ this.pending.set(id, { resolve, reject, timer });
209
+ try {
210
+ this.send({ id, method, params });
211
+ } catch (error) {
212
+ this.fail(error as Error);
213
+ }
214
+ });
215
+ }
216
+ notify(method: string, params: RpcObject): void {
217
+ this.send({ method, params });
218
+ }
219
+ private fail(error: Error): void {
220
+ if (this.failure || this.stopping) return;
221
+ this.failure = error;
222
+ for (const waiter of this.pending.values()) {
223
+ clearTimeout(waiter.timer);
224
+ waiter.reject(error);
225
+ }
226
+ this.pending.clear();
227
+ this.process.kill('SIGKILL');
228
+ // A failed persistence observer must not leave the transport usable or make a timer throw.
229
+ // Pending callers already reject; the committed dispatch intent remains for recovery.
230
+ try {
231
+ this.handlers.failed?.(error);
232
+ } catch {}
233
+ }
234
+ async close(): Promise<void> {
235
+ this.stopping = true;
236
+ for (const waiter of this.pending.values()) {
237
+ clearTimeout(waiter.timer);
238
+ waiter.reject(new Error('Harness connection closed'));
239
+ }
240
+ this.pending.clear();
241
+ this.process.kill('SIGKILL');
242
+ await this.exited;
243
+ }
244
+ }
@@ -0,0 +1,91 @@
1
+ import { HARNESS_TARGETS, type HarnessTarget } from './harness-probe.js';
2
+ import { ServiceError } from './protocol.js';
3
+ /** Correlation only: these IDs never authenticate a worker or grant operator authority. */
4
+ export function harnessToolIds(
5
+ target: HarnessTarget,
6
+ version: string,
7
+ intent: { id: string; name: string },
8
+ ): string[] {
9
+ if (
10
+ !Object.hasOwn(HARNESS_TARGETS, target) ||
11
+ HARNESS_TARGETS[target].version !== version ||
12
+ !/^[a-zA-Z0-9_-]{1,128}$/.test(intent.id) ||
13
+ !/^[a-zA-Z0-9_-]{1,128}$/.test(intent.name)
14
+ )
15
+ throw new ServiceError(
16
+ 'harness_tool_identity',
17
+ 'Unproved runtime or malformed model tool identity',
18
+ );
19
+ // Gemini 0.64.0 namespaces model-call IDs when projecting them into ACP tool calls.
20
+ return target === 'gemini-acp' ? [intent.id, intent.name + '__' + intent.id] : [intent.id];
21
+ }
22
+
23
+ /** Parse only literal shell argv displays; never evaluate expansions, redirections or operators. */
24
+ function literalWords(display: string): string[] | null {
25
+ const words: string[] = [];
26
+ let word = '',
27
+ quote = '',
28
+ present = false;
29
+ for (let i = 0; i < display.length; i++) {
30
+ const c = display[i]!;
31
+ if (quote === "'") {
32
+ if (c === "'") quote = '';
33
+ else word += c;
34
+ } else if (c === '\\') {
35
+ const next = display[++i];
36
+ if (next === undefined || next === '\n') return null;
37
+ if (quote === '"' && !['"', '\\', '$', '`'].includes(next)) word += '\\';
38
+ word += next;
39
+ present = true;
40
+ } else if (quote === '"') {
41
+ if (c === '"') quote = '';
42
+ else if (c === '$' || c === '`') return null;
43
+ else word += c;
44
+ } else if (c === "'" || c === '"') {
45
+ quote = c;
46
+ present = true;
47
+ } else if (c === '\n' || c === '\r') return null;
48
+ else if (/\s/.test(c)) {
49
+ if (present) words.push(word);
50
+ word = '';
51
+ present = false;
52
+ } else {
53
+ if (';&|<>()$`*?[]{}~#'.includes(c)) return null;
54
+ word += c;
55
+ present = true;
56
+ }
57
+ }
58
+ if (quote) return null;
59
+ if (present) words.push(word);
60
+ return words;
61
+ }
62
+
63
+ /** Correlate the pinned app-server's concrete shell display with an independently captured model call. No grant is implied. */
64
+ export function matchesCodexCommandScope(
65
+ version: string,
66
+ intent: { id: string; name: string; arguments: Record<string, unknown> } | undefined,
67
+ params: Record<string, unknown>,
68
+ ): boolean {
69
+ if (
70
+ version !== HARNESS_TARGETS['codex-app-server'].version ||
71
+ !intent ||
72
+ intent.name !== 'exec_command' ||
73
+ intent.id !== params.itemId ||
74
+ typeof intent.arguments.cmd !== 'string' ||
75
+ typeof params.command !== 'string' ||
76
+ typeof intent.arguments.workdir !== 'string' ||
77
+ params.cwd !== intent.arguments.workdir ||
78
+ intent.arguments.shell !== undefined ||
79
+ intent.arguments.login === false
80
+ )
81
+ return false;
82
+ const words = literalWords(params.command);
83
+ return (
84
+ words?.length === 3 &&
85
+ // The pinned image exposes Bash at both paths. Codex selects /bin/bash
86
+ // for its UID-1000 passwd entry and /usr/bin/bash for an unmapped UID.
87
+ ['/usr/bin/bash', '/bin/bash'].includes(words[0]!) &&
88
+ words[1] === '-lc' &&
89
+ words[2] === intent.arguments.cmd
90
+ );
91
+ }