Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/platform/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,18 @@ export const assertInternalRequest = (
headers: Record<string, string | string[] | undefined>,
token = loadPlatformConfig().internalServiceToken
) => {
const internalToken = headers["x-jeanbot-internal-token"];
if (!internalToken || internalToken !== token) {
throw new Error("Unauthorized internal service request.");
const internalTokenHeader = headers["x-jeanbot-internal-token"];
const internalToken = Array.isArray(internalTokenHeader) ? internalTokenHeader[0] : internalTokenHeader;

if (!internalToken) {
throw new Error("Unauthorized internal service request: missing token.");
}

const inputHash = crypto.createHash("sha256").update(internalToken).digest();
const targetHash = crypto.createHash("sha256").update(token).digest();

if (!crypto.timingSafeEqual(inputHash, targetHash)) {
throw new Error("Unauthorized internal service request: invalid token.");
}
};

Expand Down
4 changes: 4 additions & 0 deletions packages/security/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ export const riskFromText = (text: string): RiskLevel => {

export const redactSecrets = (input: string) => {
return input
.replace(/sk-ant-[A-Za-z0-9_-]+/g, "[REDACTED_ANTHROPIC_KEY]")
.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED_OPENAI_KEY]")
.replace(/AIza[A-Za-z0-9_-]+/g, "[REDACTED_GOOGLE_KEY]")
.replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]")
.replace(/AKIA[0-9A-Z]{16}/g, "[REDACTED_AWS_ACCESS_KEY]")
.replace(/(password|passwd|secret|private_key)(\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s"']+)/gi, "$1$2[REDACTED]")
.replace(/Bearer\s+[A-Za-z0-9._-]+/g, "Bearer [REDACTED_TOKEN]");
};

Expand Down
42 changes: 42 additions & 0 deletions packages/security/src/redact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { redactSecrets } from "./index";

Check failure on line 2 in packages/security/src/redact.test.ts

View workflow job for this annotation

GitHub Actions / ci

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './index.js'?

describe("redactSecrets", () => {
it("should redact OpenAI keys", () => {
const input = "my key is sk-12345abcde";
expect(redactSecrets(input)).toBe("my key is [REDACTED_OPENAI_KEY]");
});

it("should redact Anthropic keys", () => {
const input = "my key is sk-ant-at01-12345abcde";
expect(redactSecrets(input)).toBe("my key is [REDACTED_ANTHROPIC_KEY]");
});

it("should redact Google API keys", () => {
const input = "AIzaSyAs7890xyz";
expect(redactSecrets(input)).toBe("[REDACTED_GOOGLE_KEY]");
});

it("should redact GitHub tokens", () => {
expect(redactSecrets("ghp_1234567890")).toBe("[REDACTED_GITHUB_TOKEN]");
expect(redactSecrets("gho_1234567890")).toBe("[REDACTED_GITHUB_TOKEN]");
});

it("should redact AWS Access Keys", () => {
const input = "AKIA1234567890ABCDEF";
expect(redactSecrets(input)).toBe("[REDACTED_AWS_ACCESS_KEY]");
});

it("should redact Bearer tokens", () => {
const input = "Bearer abc.123.xyz";
expect(redactSecrets(input)).toBe("Bearer [REDACTED_TOKEN]");
});

it("should redact password fields", () => {
expect(redactSecrets("password=my-secret-pass")).toBe("password=[REDACTED]");
expect(redactSecrets("passwd: another-secret")).toBe("passwd: [REDACTED]");
expect(redactSecrets("secret=shhh")).toBe("secret=[REDACTED]");
expect(redactSecrets("password=\"quoted secret\"")).toBe("password=[REDACTED]");
expect(redactSecrets("password='single quoted'")).toBe("password=[REDACTED]");
});
});
188 changes: 118 additions & 70 deletions src/cognitive/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,81 +36,129 @@ def build_parser() -> argparse.ArgumentParser:
return parser


async def run_shell(args: argparse.Namespace):
try:
import readline # Enable history and line editing
except ImportError:
pass
class InteractiveShell:
def __init__(self, workspace_root: str, workspace_id: str, mode: str):
self.workspace_root = workspace_root
self.workspace_id = workspace_id
self.mode = mode
self.service = MissionExecutorService(
workspace_root=workspace_root,
mode=mode,
on_step_update=self.on_step_update,
)
self.last_result = None
self.history: list[str] = []
self.mission_id = f"shell-{uuid.uuid4().hex[:8]}"

def on_step_update(self, step_id: str, status: str, step: Any):
color = "\033[94m" if status == "started" else "\033[92m"
reset = "\033[0m"
print(f" {color}[{status.upper()}]{reset} {step_id}: {step.title}")

async def run(self):
try:
import readline
except ImportError:
pass

print(f"\033[1;32mJeanBot Interactive Shell\033[0m ({self.mode} mode)")
print(f"Workspace: {self.workspace_root} ({self.workspace_id})")
print("Type 'help' for commands, 'exit' to quit.")

while True:
try:
line = input("\njeanbot> ").strip()
if not line:
continue
if line.lower() in ("exit", "quit"):
print("Exiting.")
break

service = MissionExecutorService(workspace_root=args.workspace_root, mode=args.mode)
print(f"JeanBot interactive shell ({args.mode} mode)")
print(f"Workspace: {args.workspace_root} ({args.workspace_id})")
print("Type 'exit' or 'quit' to end session. Type 'help' for commands.")
self.history.append(line)

last_result = None
mission_id = f"shell-{uuid.uuid4().hex[:8]}"
history: list[str] = []
if line.lower() == "help":
self.show_help()
continue

while True:
try:
line = input("\njeanbot> ").strip()
if not line:
continue
if line.lower() in ("exit", "quit"):
if line.lower() == "history":
for i, cmd in enumerate(self.history, 1):
print(f" {i:3} {cmd}")
continue

await self.handle_objective(line)

except KeyboardInterrupt:
print("\nInterrupt received, type 'exit' to quit.")
except EOFError:
print("\nExiting.")
break
except Exception as e:
print(f"\n\033[91mError:\033[0m {e}")

def show_help(self):
print("\033[1mCommands:\033[0m")
print(" help Show this help message")
print(" history Show command history")
print(" exit | quit Exit the shell")
print(" <objective> Plan and execute a new mission")
print(" refine <feedback> Refine the last mission result with feedback")

async def handle_objective(self, line: str):
if line.lower().startswith("refine "):
if not self.last_result:
print("Nothing to refine. Run a mission first.")
return
feedback = line[7:].strip()
objective = (
f"Refine previous mission results based on: {feedback}\n"
f"Previous summary: {self.last_result.verification_summary}"
)
title = f"Refinement: {feedback[:30]}..."
else:
objective = line
title = f"Mission: {line[:30]}..."

payload = {
"mission_id": self.mission_id,
"workspace_id": self.workspace_id,
"title": title,
"objective": objective,
"mode": self.mode,
}

print(f"\033[1;34mPlanning mission:\033[0m {title}")
bundle = await self.service.plan_mission(payload)

print("\n\033[1mProposed Plan:\033[0m")
for step in bundle.record.plan.steps:
print(f" - {step.id}: {step.title} ({step.capability})")
print(f" {step.description}")

confirm = input("\nExecute this plan? [Y/n]: ").strip().lower()
if confirm and confirm != "y":
print("Execution cancelled.")
return

print(f"\n\033[1;34mExecuting mission...\033[0m")
self.last_result = await self.service.execute_bundle(bundle)

print(f"\n\033[1;32mMission Completed\033[0m")
print(f"Status: {self.last_result.status}")
print(f"Summary: {self.last_result.verification_summary}")

if self.last_result.artifacts:
print(f"\033[1mArtifacts ({len(self.last_result.artifacts)}):\033[0m")
for artifact in self.last_result.artifacts:
print(f" - {artifact.title}: {artifact.path}")

history.append(line)

if line.lower() == "help":
print("Commands:")
print(" help Show this help")
print(" history Show command history")
print(" exit | quit Exit shell")
print(" <objective> Plan and execute a mission")
print(" refine <feedback> Refine the last mission result with feedback")
continue

if line.lower() == "history":
for i, cmd in enumerate(history, 1):
print(f" {i:3} {cmd}")
continue

if line.lower().startswith("refine "):
if not last_result:
print("Nothing to refine. Run a mission first.")
continue
feedback = line[7:].strip()
objective = (
f"Refine previous mission results based on: {feedback}\n"
f"Previous summary: {last_result.verification_summary}"
)
title = f"Refinement: {feedback[:30]}..."
else:
objective = line
title = f"Mission: {line[:30]}..."

payload = {
"mission_id": mission_id,
"workspace_id": args.workspace_id,
"title": title,
"objective": objective,
"mode": args.mode,
}

print(f"Executing: {title}")
last_result = await service.execute_payload(payload)

print(f"\nStatus: {last_result.status}")
print(f"Summary: {last_result.verification_summary}")
if last_result.artifacts:
print(f"Artifacts: {len(last_result.artifacts)}")
for artifact in last_result.artifacts:
print(f" - {artifact.title}: {artifact.path}")

except KeyboardInterrupt:
print("\nInterrupt received, type 'exit' to quit.")
except Exception as e:
print(f"\nError: {e}")

async def run_shell(args: argparse.Namespace):
shell = InteractiveShell(
workspace_root=args.workspace_root,
workspace_id=args.workspace_id,
mode=args.mode,
)
await shell.run()


async def run_command(args: argparse.Namespace) -> dict:
Expand Down
8 changes: 8 additions & 0 deletions src/cognitive/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,13 +467,15 @@ def __init__(
sub_agent_service: SubAgentService,
file_service: FileService,
policy_service: PolicyService,
on_step_update: Any | None = None,
):
self.runtime = runtime
self.memory_service = memory_service
self.audit_service = audit_service
self.sub_agent_service = sub_agent_service
self.file_service = file_service
self.policy_service = policy_service
self.on_step_update = on_step_update
self.intelligence = MissionExecutionIntelligence()
self.replanner = AdaptiveReplanner()

Expand Down Expand Up @@ -869,6 +871,9 @@ async def _execute_step(
) -> StepOutcome:
step_started_at = utc_now_iso()
step.status = "running"

if self.on_step_update:
self.on_step_update(step.id, "started", step)

await self.audit_service.record(
"mission.step.started",
Expand Down Expand Up @@ -915,6 +920,9 @@ async def _execute_step(
)

step.status = "completed"

if self.on_step_update:
self.on_step_update(step.id, "completed", step)

report = StepExecutionRecord(
step_id=sub_agent_result.step_report.step_id,
Expand Down
11 changes: 10 additions & 1 deletion src/cognitive/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class MissionExecutorService:
capability_risk: dict[str, str] = field(default_factory=dict)
failure_policy: dict[str, int] = field(default_factory=dict)
mode: str = "local"
on_step_update: Any | None = None

def build_bundle(self, mission_payload: dict[str, Any]) -> MissionExecutionBundle:
workspace_id = mission_payload.get("workspace_id") or mission_payload.get("workspaceId")
Expand Down Expand Up @@ -112,6 +113,7 @@ def build_bundle(self, mission_payload: dict[str, Any]) -> MissionExecutionBundl
sub_agent_service=subagent_service,
file_service=file_service,
policy_service=policy_service,
on_step_update=self.on_step_update,
)
return MissionExecutionBundle(
record=record,
Expand All @@ -125,12 +127,19 @@ def build_bundle(self, mission_payload: dict[str, Any]) -> MissionExecutionBundl
policy_service=policy_service,
)

async def execute_payload(self, mission_payload: dict[str, Any]) -> MissionRunResult:
async def plan_mission(self, mission_payload: dict[str, Any]) -> MissionExecutionBundle:
bundle = self.build_bundle(mission_payload)
return bundle

async def execute_bundle(self, bundle: MissionExecutionBundle) -> MissionRunResult:
result = await bundle.executor.execute(bundle.record, bundle.context)
self._persist_run_summary(bundle, result)
return result

async def execute_payload(self, mission_payload: dict[str, Any]) -> MissionRunResult:
bundle = await self.plan_mission(mission_payload)
return await self.execute_bundle(bundle)

async def finalize_distributed_payload(self, mission_payload: dict[str, Any]) -> MissionRunResult:
bundle = self.build_bundle(mission_payload)
bundle.record.active_execution = self._build_active_execution(mission_payload)
Expand Down
Loading