pi-extended-teams 2.1.16
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/README.md +105 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-report.ts +181 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
- package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
- package/extensions/agents/read-agent.test.ts +3077 -0
- package/extensions/agents/read-agent.ts +1119 -0
- package/extensions/agents/write-agent.test.ts +513 -0
- package/extensions/agents/write-agent.ts +392 -0
- package/extensions/events/register-events.test.ts +465 -0
- package/extensions/events/register-events.ts +409 -0
- package/extensions/index.test.ts +1659 -0
- package/extensions/index.ts +1228 -0
- package/extensions/internal/agent-session-files.test.ts +164 -0
- package/extensions/internal/agent-session-files.ts +320 -0
- package/extensions/internal/debug.ts +44 -0
- package/extensions/internal/model-selection.ts +82 -0
- package/extensions/internal/pi-command.test.ts +191 -0
- package/extensions/internal/pi-command.ts +224 -0
- package/extensions/internal/pi-runtime-api.test.ts +43 -0
- package/extensions/internal/pi-runtime-api.ts +78 -0
- package/extensions/internal/schema.ts +19 -0
- package/extensions/internal/session-context-reference.test.ts +290 -0
- package/extensions/internal/session-context-reference.ts +430 -0
- package/extensions/internal/session-files.test.ts +225 -0
- package/extensions/internal/session-files.ts +273 -0
- package/extensions/internal/session-usage.ts +59 -0
- package/extensions/resources/spawn-resource-plan.test.ts +233 -0
- package/extensions/resources/spawn-resource-plan.ts +247 -0
- package/extensions/runtime/active-agent-sleep.test.ts +157 -0
- package/extensions/runtime/active-agent-sleep.ts +117 -0
- package/extensions/runtime/nested-read-agents.ts +22 -0
- package/extensions/runtime/pending-child-controller.test.ts +169 -0
- package/extensions/runtime/pending-child-controller.ts +280 -0
- package/extensions/runtime/types.ts +68 -0
- package/extensions/team/contracts.test.ts +111 -0
- package/extensions/team/lifecycle.test.ts +1095 -0
- package/extensions/team/lifecycle.ts +502 -0
- package/extensions/team/recipient-closure.test.ts +97 -0
- package/extensions/team/recipient-closure.ts +120 -0
- package/extensions/team/roster.test.ts +129 -0
- package/extensions/team/roster.ts +165 -0
- package/extensions/team/team-contracts.json +43 -0
- package/extensions/team/writer-screens.test.ts +50 -0
- package/extensions/team/writer-screens.ts +156 -0
- package/extensions/tools/agent-communication-tools.test.ts +306 -0
- package/extensions/tools/agent-communication-tools.ts +183 -0
- package/extensions/tools/coordination-tools.test.ts +670 -0
- package/extensions/tools/coordination-tools.ts +304 -0
- package/extensions/tools/delegation-guard.test.ts +95 -0
- package/extensions/tools/delegation-guard.ts +65 -0
- package/extensions/tools/file-claim-tools.test.ts +104 -0
- package/extensions/tools/file-claim-tools.ts +68 -0
- package/extensions/tools/model-tools.ts +53 -0
- package/extensions/tools/predefined-tools.test.ts +647 -0
- package/extensions/tools/predefined-tools.ts +331 -0
- package/extensions/tools/read-helper.test.ts +35 -0
- package/extensions/tools/task-runtime-tools.test.ts +334 -0
- package/extensions/tools/task-runtime-tools.ts +227 -0
- package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
- package/extensions/tools/team-tools.ts +1270 -0
- package/extensions/ui/agent-follow-view.test.ts +520 -0
- package/extensions/ui/agent-follow-view.ts +779 -0
- package/extensions/ui/agent-navigation.test.ts +52 -0
- package/extensions/ui/agent-navigation.ts +55 -0
- package/extensions/ui/ansi.ts +16 -0
- package/extensions/ui/extensions-command.test.ts +336 -0
- package/extensions/ui/extensions-command.ts +282 -0
- package/extensions/ui/favorite-models-command.test.ts +280 -0
- package/extensions/ui/favorite-models-command.ts +423 -0
- package/extensions/ui/frame.ts +77 -0
- package/extensions/ui/input.ts +17 -0
- package/extensions/ui/read-agent-status.test.ts +117 -0
- package/extensions/ui/read-agent-status.ts +125 -0
- package/extensions/ui/renderers.ts +243 -0
- package/extensions/ui/status-widget.test.ts +143 -0
- package/extensions/ui/status-widget.ts +440 -0
- package/extensions/ui-frame.test.ts +69 -0
- package/package.json +75 -0
- package/skills/teams.md +256 -0
- package/src/adapters/terminal-registry.ts +78 -0
- package/src/adapters/tmux-adapter.test.ts +276 -0
- package/src/adapters/tmux-adapter.ts +200 -0
- package/src/orchestration/index.ts +437 -0
- package/src/orchestration/orchestrator.test.ts +396 -0
- package/src/orchestration/types.ts +125 -0
- package/src/utils/atomic-json.ts +21 -0
- package/src/utils/claims.test.ts +137 -0
- package/src/utils/claims.ts +169 -0
- package/src/utils/hooks.test.ts +75 -0
- package/src/utils/hooks.ts +35 -0
- package/src/utils/lifecycle-tombstone.test.ts +105 -0
- package/src/utils/lifecycle-tombstone.ts +268 -0
- package/src/utils/lock.race.child.ts +44 -0
- package/src/utils/lock.race.test.ts +198 -0
- package/src/utils/lock.test.ts +90 -0
- package/src/utils/lock.ts +186 -0
- package/src/utils/messaging.test.ts +337 -0
- package/src/utils/messaging.ts +441 -0
- package/src/utils/model-resolution.test.ts +231 -0
- package/src/utils/model-resolution.ts +322 -0
- package/src/utils/models.test.ts +8 -0
- package/src/utils/models.ts +115 -0
- package/src/utils/paths.ts +81 -0
- package/src/utils/predefined-teams/types.ts +48 -0
- package/src/utils/predefined-teams.save-template.test.ts +74 -0
- package/src/utils/predefined-teams.test.ts +441 -0
- package/src/utils/predefined-teams.ts +471 -0
- package/src/utils/read-helper-queue.ts +99 -0
- package/src/utils/report-events.test.ts +154 -0
- package/src/utils/report-events.ts +222 -0
- package/src/utils/runtime.test.ts +314 -0
- package/src/utils/runtime.ts +261 -0
- package/src/utils/security.test.ts +43 -0
- package/src/utils/settings.test.ts +480 -0
- package/src/utils/settings.ts +645 -0
- package/src/utils/shared-memory.test.ts +80 -0
- package/src/utils/shared-memory.ts +81 -0
- package/src/utils/tasks.race.test.ts +44 -0
- package/src/utils/tasks.test.ts +229 -0
- package/src/utils/tasks.ts +396 -0
- package/src/utils/teams.ts +231 -0
- package/src/utils/terminal-adapter.ts +103 -0
- package/src/utils/thinking-levels.test.ts +56 -0
- package/src/utils/thinking-levels.ts +47 -0
- package/src/utils/workflow-metadata.test.ts +11 -0
- package/src/utils/workflow-metadata.ts +88 -0
- package/src/utils/write-queue.test.ts +251 -0
- package/src/utils/write-queue.ts +202 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { withLock } from "./lock";
|
|
4
|
+
import { configPath, runtimeStatusPath, teamDir } from "./paths";
|
|
5
|
+
import type { TeamConfig } from "./models";
|
|
6
|
+
import { withLifecycleTombstoneLock } from "./lifecycle-tombstone";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runtime constants for health checking.
|
|
10
|
+
* Exported for configurability and testing.
|
|
11
|
+
*/
|
|
12
|
+
export const HEARTBEAT_STALE_MS = 90000; // 90 seconds
|
|
13
|
+
export const STARTUP_STALL_MS = 60000; // 60 seconds
|
|
14
|
+
export const RUNTIME_STALE_MS = 300000; // 5 minutes - files older than this are considered stale
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Structured error information for better diagnostics.
|
|
18
|
+
*/
|
|
19
|
+
export interface RuntimeError {
|
|
20
|
+
message: string;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ContextUsageSnapshot {
|
|
25
|
+
tokens: number | null;
|
|
26
|
+
contextWindow: number;
|
|
27
|
+
percent: number | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function initialContextUsage(contextWindow = 0): ContextUsageSnapshot {
|
|
31
|
+
return { tokens: 0, contextWindow, percent: 0 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AgentRuntimeStatus {
|
|
35
|
+
teamName: string;
|
|
36
|
+
agentName: string;
|
|
37
|
+
lifecycleRunId?: string;
|
|
38
|
+
pid?: number;
|
|
39
|
+
startedAt?: number;
|
|
40
|
+
lastHeartbeatAt?: number;
|
|
41
|
+
lastInboxReadAt?: number;
|
|
42
|
+
ready?: boolean;
|
|
43
|
+
currentAction?: "starting" | "thinking" | "working" | "finishing" | "done";
|
|
44
|
+
activeToolName?: string;
|
|
45
|
+
tokensUsed?: number;
|
|
46
|
+
contextUsage?: ContextUsageSnapshot;
|
|
47
|
+
latestProgress?: string;
|
|
48
|
+
progressUpdatedAt?: number;
|
|
49
|
+
lastError?: RuntimeError;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class RuntimeStatusWriteRejectedError extends Error {
|
|
53
|
+
readonly code = "RUNTIME_STATUS_WRITE_REJECTED";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isRuntimeStatusWriteRejectedError(error: unknown): error is RuntimeStatusWriteRejectedError {
|
|
57
|
+
return error instanceof RuntimeStatusWriteRejectedError
|
|
58
|
+
|| (error instanceof Error && (error as Error & { code?: string }).code === "RUNTIME_STATUS_WRITE_REJECTED");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rejectRuntimeWrite(teamName: string, agentName: string, reason: string): never {
|
|
62
|
+
throw new RuntimeStatusWriteRejectedError(
|
|
63
|
+
`Refusing runtime status write for ${agentName} in ${teamName}: ${reason}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Write runtime status for one admitted lifecycle run. Merges with existing
|
|
69
|
+
* status while holding lifecycle -> config -> runtime locks.
|
|
70
|
+
*/
|
|
71
|
+
export async function writeRuntimeStatus(
|
|
72
|
+
teamName: string,
|
|
73
|
+
agentName: string,
|
|
74
|
+
expectedRunId: string,
|
|
75
|
+
updates: Omit<Partial<AgentRuntimeStatus>, "teamName" | "agentName" | "lifecycleRunId">
|
|
76
|
+
): Promise<AgentRuntimeStatus> {
|
|
77
|
+
if (!expectedRunId) rejectRuntimeWrite(teamName, agentName, "expected lifecycle run id is missing.");
|
|
78
|
+
const p = runtimeStatusPath(teamName, agentName);
|
|
79
|
+
const teamConfigPath = configPath(teamName);
|
|
80
|
+
|
|
81
|
+
return withLifecycleTombstoneLock(teamName, agentName, async lifecycleLock => {
|
|
82
|
+
const fence = lifecycleLock.read();
|
|
83
|
+
if (fence.status === "corrupt") rejectRuntimeWrite(teamName, agentName, `lifecycle tombstone is corrupt: ${fence.error}`);
|
|
84
|
+
if (fence.status === "occupied") rejectRuntimeWrite(teamName, agentName, `lifecycle run ${fence.tombstone.runId} is ${fence.tombstone.phase}.`);
|
|
85
|
+
|
|
86
|
+
return withLock(teamConfigPath, async () => {
|
|
87
|
+
if (!fs.existsSync(teamConfigPath)) rejectRuntimeWrite(teamName, agentName, "team config is absent.");
|
|
88
|
+
let config: TeamConfig;
|
|
89
|
+
try {
|
|
90
|
+
config = JSON.parse(fs.readFileSync(teamConfigPath, "utf-8")) as TeamConfig;
|
|
91
|
+
} catch {
|
|
92
|
+
rejectRuntimeWrite(teamName, agentName, "team config is unreadable.");
|
|
93
|
+
}
|
|
94
|
+
const member = Array.isArray(config.members)
|
|
95
|
+
? config.members.find(item => item.name === agentName)
|
|
96
|
+
: undefined;
|
|
97
|
+
if (!member || member.isActive === false) rejectRuntimeWrite(teamName, agentName, "matching active roster member is absent.");
|
|
98
|
+
if (member.lifecycleRunId !== expectedRunId) {
|
|
99
|
+
rejectRuntimeWrite(teamName, agentName, `roster belongs to lifecycle run ${member.lifecycleRunId || "unknown"}, not ${expectedRunId}.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return withLock(p, async () => {
|
|
103
|
+
let current: AgentRuntimeStatus = { teamName, agentName, lifecycleRunId: expectedRunId };
|
|
104
|
+
if (fs.existsSync(p)) {
|
|
105
|
+
try {
|
|
106
|
+
current = JSON.parse(fs.readFileSync(p, "utf-8")) as AgentRuntimeStatus;
|
|
107
|
+
} catch {
|
|
108
|
+
current = { teamName, agentName, lifecycleRunId: expectedRunId };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (current.lifecycleRunId && current.lifecycleRunId !== expectedRunId) {
|
|
112
|
+
rejectRuntimeWrite(teamName, agentName, `runtime file belongs to lifecycle run ${current.lifecycleRunId}.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const next: AgentRuntimeStatus = {
|
|
116
|
+
...current,
|
|
117
|
+
...updates,
|
|
118
|
+
teamName,
|
|
119
|
+
agentName,
|
|
120
|
+
lifecycleRunId: expectedRunId,
|
|
121
|
+
};
|
|
122
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
123
|
+
fs.writeFileSync(p, JSON.stringify(next, null, 2));
|
|
124
|
+
return next;
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read runtime status for an agent. Returns null if not found.
|
|
132
|
+
*/
|
|
133
|
+
export async function readRuntimeStatus(
|
|
134
|
+
teamName: string,
|
|
135
|
+
agentName: string
|
|
136
|
+
): Promise<AgentRuntimeStatus | null> {
|
|
137
|
+
const p = runtimeStatusPath(teamName, agentName);
|
|
138
|
+
if (!fs.existsSync(p)) return null;
|
|
139
|
+
|
|
140
|
+
return await withLock(p, async () => {
|
|
141
|
+
if (!fs.existsSync(p)) return null;
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(fs.readFileSync(p, "utf-8")) as AgentRuntimeStatus;
|
|
144
|
+
} catch {
|
|
145
|
+
// Corrupted file
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Delete runtime status for an agent. Called during shutdown.
|
|
153
|
+
*/
|
|
154
|
+
async function deleteRuntimeStatusFile(
|
|
155
|
+
teamName: string,
|
|
156
|
+
agentName: string,
|
|
157
|
+
expectedRunId?: string
|
|
158
|
+
): Promise<boolean> {
|
|
159
|
+
const p = runtimeStatusPath(teamName, agentName);
|
|
160
|
+
if (!fs.existsSync(p)) return false;
|
|
161
|
+
|
|
162
|
+
return withLock(p, async () => {
|
|
163
|
+
if (!fs.existsSync(p)) return false;
|
|
164
|
+
if (expectedRunId) {
|
|
165
|
+
let current: AgentRuntimeStatus;
|
|
166
|
+
try {
|
|
167
|
+
current = JSON.parse(fs.readFileSync(p, "utf-8")) as AgentRuntimeStatus;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (current.lifecycleRunId !== expectedRunId) return false;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
fs.unlinkSync(p);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function deleteRuntimeStatus(
|
|
183
|
+
teamName: string,
|
|
184
|
+
agentName: string,
|
|
185
|
+
expectedRunId?: string
|
|
186
|
+
): Promise<boolean> {
|
|
187
|
+
return deleteRuntimeStatusFile(teamName, agentName, expectedRunId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Runtime-file deletion for finalizers that already hold the lifecycle lock. */
|
|
191
|
+
export async function deleteRuntimeStatusUnderLifecycleLock(
|
|
192
|
+
teamName: string,
|
|
193
|
+
agentName: string,
|
|
194
|
+
expectedRunId: string
|
|
195
|
+
): Promise<boolean> {
|
|
196
|
+
return deleteRuntimeStatusFile(teamName, agentName, expectedRunId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Clean up stale runtime files for a team.
|
|
201
|
+
* Removes files older than RUNTIME_STALE_MS that have no recent heartbeat.
|
|
202
|
+
* Returns the number of files cleaned up.
|
|
203
|
+
*/
|
|
204
|
+
export interface CleanupStaleRuntimeHooks {
|
|
205
|
+
afterLifecycleLock?(agentName: string): void | Promise<void>;
|
|
206
|
+
beforeDelete?(agentName: string): void | Promise<void>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function cleanupStaleRuntimeFiles(
|
|
210
|
+
teamName: string,
|
|
211
|
+
now: number = Date.now(),
|
|
212
|
+
hooks: CleanupStaleRuntimeHooks = {}
|
|
213
|
+
): Promise<number> {
|
|
214
|
+
const runtimeDir = path.join(teamDir(teamName), "runtime");
|
|
215
|
+
if (!fs.existsSync(runtimeDir)) return 0;
|
|
216
|
+
|
|
217
|
+
let cleaned = 0;
|
|
218
|
+
const files = fs.readdirSync(runtimeDir).filter(f => f.endsWith(".json"));
|
|
219
|
+
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
const p = path.join(runtimeDir, file);
|
|
222
|
+
const agentName = file.slice(0, -".json".length);
|
|
223
|
+
await withLifecycleTombstoneLock(teamName, agentName, async lifecycleLock => {
|
|
224
|
+
await hooks.afterLifecycleLock?.(agentName);
|
|
225
|
+
await withLock(p, async () => {
|
|
226
|
+
const fence = lifecycleLock.read();
|
|
227
|
+
if (fence.status !== "absent" || !fs.existsSync(p)) return;
|
|
228
|
+
|
|
229
|
+
let shouldDelete = false;
|
|
230
|
+
try {
|
|
231
|
+
const status = JSON.parse(fs.readFileSync(p, "utf-8")) as AgentRuntimeStatus;
|
|
232
|
+
const lastActivity = status.lastHeartbeatAt || status.startedAt || 0;
|
|
233
|
+
shouldDelete = (now - lastActivity) > RUNTIME_STALE_MS;
|
|
234
|
+
} catch {
|
|
235
|
+
shouldDelete = true;
|
|
236
|
+
}
|
|
237
|
+
if (!shouldDelete) return;
|
|
238
|
+
|
|
239
|
+
await hooks.beforeDelete?.(agentName);
|
|
240
|
+
try {
|
|
241
|
+
fs.unlinkSync(p);
|
|
242
|
+
cleaned++;
|
|
243
|
+
} catch {
|
|
244
|
+
// Best-effort janitor; another cleanup may already have removed it.
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return cleaned;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create a structured error object from an error.
|
|
255
|
+
*/
|
|
256
|
+
export function createRuntimeError(error: unknown): RuntimeError {
|
|
257
|
+
return {
|
|
258
|
+
message: error instanceof Error ? error.message : String(error),
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { teamDir, inboxPath, sanitizeName } from "./paths";
|
|
6
|
+
|
|
7
|
+
describe("Security Audit - Path Traversal (Prevention Check)", () => {
|
|
8
|
+
it("should throw an error for path traversal via teamName", () => {
|
|
9
|
+
const maliciousTeamName = "../../etc";
|
|
10
|
+
expect(() => teamDir(maliciousTeamName)).toThrow();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should throw an error for path traversal via agentName", () => {
|
|
14
|
+
const teamName = "audit-team";
|
|
15
|
+
const maliciousAgentName = "../../../.ssh/id_rsa";
|
|
16
|
+
expect(() => inboxPath(teamName, maliciousAgentName)).toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should throw an error for path traversal via taskId", () => {
|
|
20
|
+
const teamName = "audit-team";
|
|
21
|
+
const maliciousTaskId = "../../../etc/passwd";
|
|
22
|
+
// We need to import readTask/updateTask or just sanitizeName directly if we want to test the logic
|
|
23
|
+
// But since we already tested sanitizeName via other paths, this is just for completeness.
|
|
24
|
+
expect(() => sanitizeName(maliciousTaskId)).toThrow();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("Security Audit - Command Injection (Fixed)", () => {
|
|
29
|
+
it("should not be vulnerable to command injection in spawn_teammate (via parameters)", () => {
|
|
30
|
+
const maliciousCwd = "; rm -rf / ;";
|
|
31
|
+
const name = "attacker";
|
|
32
|
+
const team_name = "audit-team";
|
|
33
|
+
const piBinary = "pi";
|
|
34
|
+
const cmd = `PI_TEAM_NAME=${team_name} PI_AGENT_NAME=${name} ${piBinary}`;
|
|
35
|
+
|
|
36
|
+
// Simulating what happens in spawn_teammate (extensions/index.ts)
|
|
37
|
+
const itermCmd = `cd '${maliciousCwd}' && ${cmd}`;
|
|
38
|
+
|
|
39
|
+
// The command becomes: cd '; rm -rf / ;' && PI_TEAM_NAME=audit-team PI_AGENT_NAME=attacker pi
|
|
40
|
+
expect(itermCmd).toContain("cd '; rm -rf / ;' &&");
|
|
41
|
+
expect(itermCmd).not.toContain("cd ; rm -rf / ; &&");
|
|
42
|
+
});
|
|
43
|
+
});
|