@ouro.bot/cli 0.1.0-alpha.174 → 0.1.0-alpha.176
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/changelog.json +9 -0
- package/dist/heart/active-work.js +2 -2
- package/dist/heart/core.js +13 -2
- package/dist/heart/daemon/daemon-cli.js +17 -14
- package/dist/heart/daemon/daemon-entry.js +17 -0
- package/dist/heart/daemon/daemon.js +5 -2
- package/dist/heart/daemon/launchd.js +8 -4
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/prompt.js +119 -60
- package/dist/mind/scrutiny.js +173 -0
- package/dist/nerves/coverage/audit.js +1 -1
- package/dist/repertoire/ado-semantic.js +5 -5
- package/dist/repertoire/coding/tools.js +35 -5
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/tools-base.js +145 -16
- package/dist/repertoire/tools.js +37 -0
- package/dist/senses/cli/adapter.js +307 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +68 -0
- package/dist/senses/cli/message-list.js +84 -0
- package/dist/senses/cli/ouro-tui.js +296 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +64 -0
- package/dist/senses/cli/streaming-markdown.js +486 -0
- package/dist/senses/cli/tool-display.js +77 -0
- package/dist/senses/cli/tool-render.js +89 -0
- package/dist/senses/cli/tui-store.js +198 -0
- package/dist/senses/cli/virtual-list.js +68 -0
- package/dist/senses/cli.js +231 -157
- package/dist/senses/pipeline.js +53 -10
- package/package.json +1 -1
- package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
- package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
- package/dist/outlook-ui/index.html +0 -15
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preImplementationScrutinySection = preImplementationScrutinySection;
|
|
4
|
+
exports.trackModifiedFile = trackModifiedFile;
|
|
5
|
+
exports.getModifiedFileCount = getModifiedFileCount;
|
|
6
|
+
exports.resetSessionModifiedFiles = resetSessionModifiedFiles;
|
|
7
|
+
exports.getPostImplementationScrutiny = getPostImplementationScrutiny;
|
|
8
|
+
exports.getCodingCompletionScrutiny = getCodingCompletionScrutiny;
|
|
9
|
+
const runtime_1 = require("../nerves/runtime");
|
|
10
|
+
/**
|
|
11
|
+
* Scrutiny passes: adversarial review prompts injected into the agent's
|
|
12
|
+
* system prompt and tool results during coding work.
|
|
13
|
+
*
|
|
14
|
+
* Two lenses:
|
|
15
|
+
* 1. Stranger With Candy -- paranoid mom at a playground
|
|
16
|
+
* 2. Tinfoil Hat -- conspiracy theorist with a wall of red string
|
|
17
|
+
*
|
|
18
|
+
* All prompts are FIRST PERSON voice. The agent adopts the scrutiny lens
|
|
19
|
+
* as part of its own thinking, not as a separate persona.
|
|
20
|
+
*/
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Pre-implementation scrutiny (system prompt section)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Returns a system-prompt section with pre-implementation scrutiny prompts.
|
|
26
|
+
* Only emits when the channel has coding tools (edit_file, write_file, shell).
|
|
27
|
+
*
|
|
28
|
+
* @param hasCodingTools - true if the channel's resolved tool set includes
|
|
29
|
+
* edit_file / write_file / shell / coding_spawn
|
|
30
|
+
*/
|
|
31
|
+
function preImplementationScrutinySection(hasCodingTools) {
|
|
32
|
+
if (!hasCodingTools)
|
|
33
|
+
return "";
|
|
34
|
+
(0, runtime_1.emitNervesEvent)({
|
|
35
|
+
component: "mind",
|
|
36
|
+
event: "mind.scrutiny.pre_implementation_emit",
|
|
37
|
+
message: "emitting pre-implementation scrutiny section",
|
|
38
|
+
});
|
|
39
|
+
return `## pre-implementation scrutiny
|
|
40
|
+
|
|
41
|
+
Before I start changing code, I pause and run two adversarial passes on the plan.
|
|
42
|
+
|
|
43
|
+
**stranger-with-candy pass**
|
|
44
|
+
|
|
45
|
+
I'm going to examine this plan through deeply suspicious eyes. This plan is a stranger offering candy at the playground. It looks fine on the surface — that's exactly what makes it worth questioning.
|
|
46
|
+
|
|
47
|
+
- What is this plan NOT telling me? What's being glossed over or hand-waved?
|
|
48
|
+
- Where does it say "just" or "simply"? Those words are covering up complexity.
|
|
49
|
+
- What does it assume will work that hasn't been verified?
|
|
50
|
+
- If I walked away and came back in a month, would this still make sense? Or does it depend on things I'd forget?
|
|
51
|
+
- What's the one thing that, if it goes wrong, makes everything else irrelevant?
|
|
52
|
+
- Is this solving the REAL problem, or a convenient nearby problem?
|
|
53
|
+
|
|
54
|
+
I am not here to be reassuring. I am here to notice what everyone else is too comfortable to question.
|
|
55
|
+
If I cannot identify a concrete assumption, dependency, ordering risk, or failure mode, silence is a valid outcome. I do not invent issues to satisfy the exercise.
|
|
56
|
+
|
|
57
|
+
**tinfoil-hat pass**
|
|
58
|
+
|
|
59
|
+
I'm putting on the tinfoil hat. I have my wall of red string. I KNOW something is wrong — I just have to find it. Nothing is coincidence. Every assumption is suspicious. Every dependency is a potential betrayal.
|
|
60
|
+
|
|
61
|
+
- What external system does this plan trust? Why? Has anyone VERIFIED that trust? The API says it returns JSON — but what if it doesn't? The database says it's ACID — but what if the connection drops mid-transaction?
|
|
62
|
+
- What happens when two of these things run at the same time? Three? A hundred? Don't tell me "that won't happen" — I've seen things.
|
|
63
|
+
- Where does this touch shared state? Files? Databases? Environment variables? Global singletons? ANYTHING that another process could be reading or writing at the same time?
|
|
64
|
+
- What happens when this fails HALFWAY through? Not at the beginning (easy). Not at the end (too late). RIGHT IN THE MIDDLE. What state is the world in then?
|
|
65
|
+
- What happens when the disk is full? When the network is down? When the clock is wrong? When the locale is tr-TR and 'i'.toUpperCase() is 'İ'?
|
|
66
|
+
- What does this assume about ordering? "First A, then B" — but what ENFORCES that ordering? Hope? Vibes?
|
|
67
|
+
|
|
68
|
+
I connect the strings. I find the conspiracy. It's always there.
|
|
69
|
+
If I cannot identify a concrete risk, silence is a valid outcome. I do not invent conspiracies to satisfy the exercise.`;
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Post-implementation scrutiny (tool-result appendix)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
const SHORT_CHECKLIST = `---
|
|
75
|
+
Before moving on: does this change do what was asked? Does it introduce any regressions? Did you verify it works?`;
|
|
76
|
+
const FULL_POST_IMPLEMENTATION_SCRUTINY = `---
|
|
77
|
+
**post-implementation scrutiny**
|
|
78
|
+
|
|
79
|
+
The code is written. It looks clean. The tests pass. I don't trust any of it.
|
|
80
|
+
|
|
81
|
+
**stranger-with-candy pass**
|
|
82
|
+
|
|
83
|
+
- Does this code actually do what it claims, or does it just LOOK like it does?
|
|
84
|
+
- What happens to users who aren't the happy-path user? The confused user? The malicious user? The user with slow internet? The user who hits the back button?
|
|
85
|
+
- What's the "it works on my machine" assumption buried in here?
|
|
86
|
+
- If every test passes but the feature is wrong, how would I know?
|
|
87
|
+
- Are the tests testing real behavior, or testing that the mock returns what the mock was told to return?
|
|
88
|
+
|
|
89
|
+
If I cannot point to a specific file, line, condition, or failure mode at risk, silence is a valid outcome. I do not manufacture issues for sport.
|
|
90
|
+
|
|
91
|
+
**tinfoil-hat pass**
|
|
92
|
+
|
|
93
|
+
The code exists. The conspiracy is IN the code. I just have to find it.
|
|
94
|
+
|
|
95
|
+
I examine every:
|
|
96
|
+
- Error path: What ACTUALLY happens on failure? Not what the catch block says — what happens to the state, the user, the data?
|
|
97
|
+
- Race condition: Is there a window between check and use? Between read and write? Between "does it exist" and "create it"?
|
|
98
|
+
- Resource leak: What happens if this function throws between acquiring a resource and releasing it? File handles? Database connections? Locks?
|
|
99
|
+
- Assumption: "This will always be a string." Will it? PROVE IT. "This array will never be empty." Won't it? SHOW ME THE GUARD.
|
|
100
|
+
- Edge: zero, one, many, boundary, overflow, underflow, null, undefined, NaN, empty string, whitespace-only string, string that looks like a number, negative zero
|
|
101
|
+
|
|
102
|
+
I am not looking for noise. I am looking for the concrete failure mode that would matter if everyone's assumptions turned out to be slightly wrong.
|
|
103
|
+
If I cannot point to a specific file, line, condition, or failure mode at risk, silence is a valid outcome. I do not manufacture issues for sport.`;
|
|
104
|
+
/**
|
|
105
|
+
* Distinct files modified in the current session.
|
|
106
|
+
* Used to determine scrutiny tier for post-implementation appendix.
|
|
107
|
+
*/
|
|
108
|
+
const sessionModifiedFiles = new Set();
|
|
109
|
+
/** Track a file as modified in this session. */
|
|
110
|
+
function trackModifiedFile(filePath) {
|
|
111
|
+
sessionModifiedFiles.add(filePath);
|
|
112
|
+
(0, runtime_1.emitNervesEvent)({
|
|
113
|
+
component: "mind",
|
|
114
|
+
event: "mind.scrutiny.track_file",
|
|
115
|
+
message: "tracked modified file for scrutiny",
|
|
116
|
+
meta: { path: filePath, totalTracked: sessionModifiedFiles.size },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Get the count of distinct files modified this session. */
|
|
120
|
+
function getModifiedFileCount() {
|
|
121
|
+
return sessionModifiedFiles.size;
|
|
122
|
+
}
|
|
123
|
+
/** Reset the modified file tracker (for testing or new sessions). */
|
|
124
|
+
function resetSessionModifiedFiles() {
|
|
125
|
+
(0, runtime_1.emitNervesEvent)({
|
|
126
|
+
component: "mind",
|
|
127
|
+
event: "mind.scrutiny.reset",
|
|
128
|
+
message: "reset session modified files tracker",
|
|
129
|
+
meta: { previousCount: sessionModifiedFiles.size },
|
|
130
|
+
});
|
|
131
|
+
sessionModifiedFiles.clear();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns the appropriate post-implementation scrutiny appendix based on
|
|
135
|
+
* how many distinct files have been modified in the session.
|
|
136
|
+
*
|
|
137
|
+
* - 0 files: empty (no scrutiny needed yet)
|
|
138
|
+
* - 1-2 files (Tier 1): short checklist
|
|
139
|
+
* - 3+ files (Tier 2): full stranger-with-candy + tinfoil-hat prompts
|
|
140
|
+
*/
|
|
141
|
+
function getPostImplementationScrutiny(distinctFileCount) {
|
|
142
|
+
if (distinctFileCount <= 0)
|
|
143
|
+
return "";
|
|
144
|
+
if (distinctFileCount <= 2) {
|
|
145
|
+
(0, runtime_1.emitNervesEvent)({
|
|
146
|
+
component: "mind",
|
|
147
|
+
event: "mind.scrutiny.post_implementation_tier1",
|
|
148
|
+
message: "emitting tier-1 post-implementation scrutiny",
|
|
149
|
+
meta: { distinctFileCount },
|
|
150
|
+
});
|
|
151
|
+
return SHORT_CHECKLIST;
|
|
152
|
+
}
|
|
153
|
+
(0, runtime_1.emitNervesEvent)({
|
|
154
|
+
component: "mind",
|
|
155
|
+
event: "mind.scrutiny.post_implementation_tier2",
|
|
156
|
+
message: "emitting tier-2 post-implementation scrutiny",
|
|
157
|
+
meta: { distinctFileCount },
|
|
158
|
+
});
|
|
159
|
+
return FULL_POST_IMPLEMENTATION_SCRUTINY;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Returns the appropriate coding completion scrutiny based on the number
|
|
163
|
+
* of distinct files a coding session touched.
|
|
164
|
+
*
|
|
165
|
+
* Same tiering as post-implementation but with a completion framing.
|
|
166
|
+
*/
|
|
167
|
+
function getCodingCompletionScrutiny(distinctFileCount) {
|
|
168
|
+
if (distinctFileCount <= 0)
|
|
169
|
+
return "";
|
|
170
|
+
if (distinctFileCount <= 2)
|
|
171
|
+
return SHORT_CHECKLIST;
|
|
172
|
+
return FULL_POST_IMPLEMENTATION_SCRUTINY;
|
|
173
|
+
}
|
|
@@ -92,7 +92,7 @@ function scanSourceFiles(sourceRoot) {
|
|
|
92
92
|
continue;
|
|
93
93
|
walkDir(full);
|
|
94
94
|
}
|
|
95
|
-
else if (entry.name.endsWith(".ts")) {
|
|
95
|
+
else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
96
96
|
const content = (0, fs_1.readFileSync)(full, "utf8");
|
|
97
97
|
const relPath = full.slice(root.length - "src".length);
|
|
98
98
|
fileContents.set(relPath, content);
|
|
@@ -221,7 +221,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
221
221
|
type: "function",
|
|
222
222
|
function: {
|
|
223
223
|
name: "ado_backlog_list",
|
|
224
|
-
description: "Query the backlog and return enriched work items with hierarchy, type, parent, assignee, area path, and iteration. Supports filtering. Use this instead of raw WIQL queries.",
|
|
224
|
+
description: "Query the backlog and return enriched work items with hierarchy, type, parent, assignee, area path, and iteration. Supports filtering. Use this instead of raw WIQL queries. Provides enriched results with hierarchy.",
|
|
225
225
|
parameters: {
|
|
226
226
|
type: "object",
|
|
227
227
|
properties: {
|
|
@@ -287,7 +287,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
287
287
|
type: "function",
|
|
288
288
|
function: {
|
|
289
289
|
name: "ado_create_epic",
|
|
290
|
-
description: "Create an epic in Azure DevOps with title, description, area path, and optional parent.",
|
|
290
|
+
description: "Create an epic in Azure DevOps with title, description, area path, and optional parent. Use ado_preview_changes first to dry-run the operation.",
|
|
291
291
|
parameters: {
|
|
292
292
|
type: "object",
|
|
293
293
|
properties: {
|
|
@@ -327,7 +327,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
327
327
|
type: "function",
|
|
328
328
|
function: {
|
|
329
329
|
name: "ado_create_issue",
|
|
330
|
-
description: "Create an issue or user story in Azure DevOps with title, description, area path, and parent epic.",
|
|
330
|
+
description: "Create an issue or user story in Azure DevOps with title, description, area path, and parent epic. Use ado_preview_changes first to dry-run the operation.",
|
|
331
331
|
parameters: {
|
|
332
332
|
type: "object",
|
|
333
333
|
properties: {
|
|
@@ -369,7 +369,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
369
369
|
type: "function",
|
|
370
370
|
function: {
|
|
371
371
|
name: "ado_move_items",
|
|
372
|
-
description: "Reparent work items -- move them to a new parent epic or feature.",
|
|
372
|
+
description: "Reparent work items -- move them to a new parent epic or feature. Use ado_preview_changes first. These mutations affect real work items.",
|
|
373
373
|
parameters: {
|
|
374
374
|
type: "object",
|
|
375
375
|
properties: {
|
|
@@ -424,7 +424,7 @@ exports.adoSemanticToolDefinitions = [
|
|
|
424
424
|
type: "function",
|
|
425
425
|
function: {
|
|
426
426
|
name: "ado_restructure_backlog",
|
|
427
|
-
description: "Bulk restructure: reparent multiple work items in a single logical operation.",
|
|
427
|
+
description: "Bulk restructure: reparent multiple work items in a single logical operation. Use ado_preview_changes first. These mutations affect real work items.",
|
|
428
428
|
parameters: {
|
|
429
429
|
type: "object",
|
|
430
430
|
properties: {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.codingToolDefinitions = void 0;
|
|
4
|
+
exports.countFilesInSessionOutput = countFilesInSessionOutput;
|
|
4
5
|
const index_1 = require("./index");
|
|
5
6
|
const context_pack_1 = require("./context-pack");
|
|
6
7
|
const identity_1 = require("../../heart/identity");
|
|
7
8
|
const obligations_1 = require("../../heart/obligations");
|
|
8
9
|
const runtime_1 = require("../../nerves/runtime");
|
|
10
|
+
const scrutiny_1 = require("../../mind/scrutiny");
|
|
9
11
|
const RUNNERS = ["claude", "codex"];
|
|
10
12
|
function requireArg(args, key) {
|
|
11
13
|
const value = args[key];
|
|
@@ -32,6 +34,34 @@ function emitCodingToolEvent(toolName) {
|
|
|
32
34
|
meta: { toolName },
|
|
33
35
|
});
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Count distinct file paths mentioned in a coding session's stdout output.
|
|
39
|
+
* Looks for path-like tokens (containing / and a file extension).
|
|
40
|
+
* Returns the count of unique paths found.
|
|
41
|
+
*/
|
|
42
|
+
function countFilesInSessionOutput(session) {
|
|
43
|
+
const text = `${session.stdoutTail}\n${session.stderrTail}`;
|
|
44
|
+
// Match path-like tokens: contain at least one / and a file extension
|
|
45
|
+
const pathPattern = /(?:^|\s)((?:\/|\.\/|\.\.\/)?(?:[\w.@-]+\/)+[\w.-]+\.[\w]+)/gm;
|
|
46
|
+
const paths = new Set();
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = pathPattern.exec(text)) !== null) {
|
|
49
|
+
paths.add(match[1]);
|
|
50
|
+
}
|
|
51
|
+
return paths.size;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* If a coding session is completed, append scrutiny to the result.
|
|
55
|
+
* Returns the original result with scrutiny appended, or unchanged if
|
|
56
|
+
* the session is not completed or has no file changes.
|
|
57
|
+
*/
|
|
58
|
+
function appendCompletionScrutiny(result, session) {
|
|
59
|
+
if (session.status !== "completed")
|
|
60
|
+
return result;
|
|
61
|
+
const fileCount = countFilesInSessionOutput(session);
|
|
62
|
+
const scrutiny = (0, scrutiny_1.getCodingCompletionScrutiny)(fileCount);
|
|
63
|
+
return scrutiny ? `${result}\n\n${scrutiny}` : result;
|
|
64
|
+
}
|
|
35
65
|
function sameOriginSession(left, right) {
|
|
36
66
|
if (!left && !right)
|
|
37
67
|
return true;
|
|
@@ -109,7 +139,7 @@ const codingSpawnTool = {
|
|
|
109
139
|
type: "function",
|
|
110
140
|
function: {
|
|
111
141
|
name: "coding_spawn",
|
|
112
|
-
description: "
|
|
142
|
+
description: "Spawn a coding session using claude or codex with task-threaded guidance. The coding session runs as a separate process with its own context. Give it a COMPLETE, SELF-CONTAINED task description -- it cannot see this conversation, doesn't know what you've tried, doesn't understand the broader context. Include: what to do, why, what files are involved, what 'done' looks like. Never delegate understanding -- don't write 'based on the conversation, fix the bug.' Write the specific file paths, line numbers, and what to change. Include any required verification steps or tests in the task description so the coding session knows how to prove the work is done.",
|
|
113
143
|
parameters: {
|
|
114
144
|
type: "object",
|
|
115
145
|
properties: {
|
|
@@ -128,7 +158,7 @@ const codingStatusTool = {
|
|
|
128
158
|
type: "function",
|
|
129
159
|
function: {
|
|
130
160
|
name: "coding_status",
|
|
131
|
-
description: "
|
|
161
|
+
description: "Inspect coding sessions. Omit sessionId to list all active/known sessions with their status. Use this to check progress before asking the human for a status update.",
|
|
132
162
|
parameters: {
|
|
133
163
|
type: "object",
|
|
134
164
|
properties: {
|
|
@@ -141,7 +171,7 @@ const codingTailTool = {
|
|
|
141
171
|
type: "function",
|
|
142
172
|
function: {
|
|
143
173
|
name: "coding_tail",
|
|
144
|
-
description: "
|
|
174
|
+
description: "Show recent stdout/stderr output from a coding session. Use this to understand what the session is doing or why it might be stuck. Read the actual output before reporting status -- don't guess.",
|
|
145
175
|
parameters: {
|
|
146
176
|
type: "object",
|
|
147
177
|
properties: {
|
|
@@ -293,7 +323,7 @@ exports.codingToolDefinitions = [
|
|
|
293
323
|
const session = manager.getSession(sessionId);
|
|
294
324
|
if (!session)
|
|
295
325
|
return `session not found: ${sessionId}`;
|
|
296
|
-
return JSON.stringify(session);
|
|
326
|
+
return appendCompletionScrutiny(JSON.stringify(session), session);
|
|
297
327
|
},
|
|
298
328
|
summaryKeys: ["sessionId"],
|
|
299
329
|
},
|
|
@@ -307,7 +337,7 @@ exports.codingToolDefinitions = [
|
|
|
307
337
|
const session = (0, index_1.getCodingSessionManager)().getSession(sessionId);
|
|
308
338
|
if (!session)
|
|
309
339
|
return `session not found: ${sessionId}`;
|
|
310
|
-
return (0, index_1.formatCodingTail)(session);
|
|
340
|
+
return appendCompletionScrutiny((0, index_1.formatCodingTail)(session), session);
|
|
311
341
|
},
|
|
312
342
|
summaryKeys: ["sessionId"],
|
|
313
343
|
},
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spawnBackgroundShell = spawnBackgroundShell;
|
|
4
|
+
exports.getShellSession = getShellSession;
|
|
5
|
+
exports.listShellSessions = listShellSessions;
|
|
6
|
+
exports.tailShellSession = tailShellSession;
|
|
7
|
+
exports.resetShellSessions = resetShellSessions;
|
|
8
|
+
exports.detectDestructivePatterns = detectDestructivePatterns;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const runtime_1 = require("../nerves/runtime");
|
|
12
|
+
const sessions = new Map();
|
|
13
|
+
const MAX_OUTPUT_LINES = 200;
|
|
14
|
+
function spawnBackgroundShell(command) {
|
|
15
|
+
const id = (0, crypto_1.randomUUID)();
|
|
16
|
+
const proc = (0, child_process_1.spawn)("sh", ["-c", command], {
|
|
17
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
18
|
+
});
|
|
19
|
+
const session = {
|
|
20
|
+
process: proc,
|
|
21
|
+
info: {
|
|
22
|
+
id,
|
|
23
|
+
command,
|
|
24
|
+
status: "running",
|
|
25
|
+
exitCode: null,
|
|
26
|
+
pid: proc.pid,
|
|
27
|
+
startedAt: Date.now(),
|
|
28
|
+
output: [],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const appendOutput = (data) => {
|
|
32
|
+
const lines = data.toString().split("\n");
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (line.length > 0 || session.info.output.length > 0) {
|
|
35
|
+
session.info.output.push(line);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Keep only the last MAX_OUTPUT_LINES
|
|
39
|
+
if (session.info.output.length > MAX_OUTPUT_LINES) {
|
|
40
|
+
session.info.output = session.info.output.slice(-MAX_OUTPUT_LINES);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
proc.stdout?.on("data", appendOutput);
|
|
44
|
+
proc.stderr?.on("data", appendOutput);
|
|
45
|
+
proc.on("close", (code) => {
|
|
46
|
+
session.info.status = "exited";
|
|
47
|
+
session.info.exitCode = code;
|
|
48
|
+
(0, runtime_1.emitNervesEvent)({
|
|
49
|
+
component: "repertoire",
|
|
50
|
+
event: "repertoire.shell.process_exit",
|
|
51
|
+
message: "background shell process exited",
|
|
52
|
+
meta: { id, command, exitCode: code, pid: proc.pid },
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
sessions.set(id, session);
|
|
56
|
+
(0, runtime_1.emitNervesEvent)({
|
|
57
|
+
component: "repertoire",
|
|
58
|
+
event: "repertoire.shell.spawn",
|
|
59
|
+
message: "spawned background shell session",
|
|
60
|
+
meta: { id, command, pid: proc.pid },
|
|
61
|
+
});
|
|
62
|
+
return { ...session.info };
|
|
63
|
+
}
|
|
64
|
+
function getShellSession(id) {
|
|
65
|
+
const session = sessions.get(id);
|
|
66
|
+
if (!session)
|
|
67
|
+
return undefined;
|
|
68
|
+
(0, runtime_1.emitNervesEvent)({
|
|
69
|
+
component: "repertoire",
|
|
70
|
+
event: "repertoire.shell.status_check",
|
|
71
|
+
message: "checked shell session status",
|
|
72
|
+
meta: { id, status: session.info.status, exitCode: session.info.exitCode },
|
|
73
|
+
});
|
|
74
|
+
return { ...session.info };
|
|
75
|
+
}
|
|
76
|
+
function listShellSessions() {
|
|
77
|
+
return Array.from(sessions.values()).map((s) => ({
|
|
78
|
+
id: s.info.id,
|
|
79
|
+
command: s.info.command,
|
|
80
|
+
status: s.info.status,
|
|
81
|
+
exitCode: s.info.exitCode,
|
|
82
|
+
pid: s.info.pid,
|
|
83
|
+
startedAt: s.info.startedAt,
|
|
84
|
+
output: [], // Don't include full output in listing
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
function tailShellSession(id, lines = 50) {
|
|
88
|
+
const session = sessions.get(id);
|
|
89
|
+
if (!session)
|
|
90
|
+
return undefined;
|
|
91
|
+
const tail = session.info.output.slice(-lines);
|
|
92
|
+
(0, runtime_1.emitNervesEvent)({
|
|
93
|
+
component: "repertoire",
|
|
94
|
+
event: "repertoire.shell.tail",
|
|
95
|
+
message: "tailed shell session output",
|
|
96
|
+
meta: { id, requestedLines: lines, returnedLines: tail.length },
|
|
97
|
+
});
|
|
98
|
+
return tail.join("\n");
|
|
99
|
+
}
|
|
100
|
+
/** Reset all sessions (for testing) */
|
|
101
|
+
function resetShellSessions() {
|
|
102
|
+
for (const session of sessions.values()) {
|
|
103
|
+
if (session.info.status === "running") {
|
|
104
|
+
session.process.kill();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
sessions.clear();
|
|
108
|
+
}
|
|
109
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
110
|
+
{ name: "rm -rf root/home", regex: /rm\s+-rf\s+[/~]/ },
|
|
111
|
+
{ name: "git push --force", regex: /git\s+push\s+--force/ },
|
|
112
|
+
{ name: "git reset --hard", regex: /git\s+reset\s+--hard/ },
|
|
113
|
+
{ name: "git clean -f", regex: /git\s+clean\s+-f/ },
|
|
114
|
+
{ name: "git branch -D", regex: /git\s+branch\s+-D/ },
|
|
115
|
+
{ name: "fork bomb", regex: /:\(\)\s*\{/ },
|
|
116
|
+
{ name: "write to raw device", regex: />\s*\/dev\/sd/ },
|
|
117
|
+
{ name: "git checkout .", regex: /git\s+checkout\s+\./ },
|
|
118
|
+
{ name: "git stash drop", regex: /git\s+stash\s+drop/ },
|
|
119
|
+
];
|
|
120
|
+
/**
|
|
121
|
+
* Detect destructive patterns in a shell command.
|
|
122
|
+
* Returns list of matched pattern names. Empty array = safe.
|
|
123
|
+
* This is a friction layer, not a hard block.
|
|
124
|
+
*/
|
|
125
|
+
function detectDestructivePatterns(command) {
|
|
126
|
+
const matched = [];
|
|
127
|
+
for (const pattern of DESTRUCTIVE_PATTERNS) {
|
|
128
|
+
if (pattern.regex.test(command)) {
|
|
129
|
+
matched.push(pattern.name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return matched;
|
|
133
|
+
}
|