diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index 1661fdb..939a0be 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -202,9 +202,18 @@ export const assertInternalRequest = ( headers: Record, 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."); } }; diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts index ce9b305..8d9f5c0 100644 --- a/packages/security/src/index.ts +++ b/packages/security/src/index.ts @@ -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]"); }; diff --git a/packages/security/src/redact.test.ts b/packages/security/src/redact.test.ts new file mode 100644 index 0000000..2fb9784 --- /dev/null +++ b/packages/security/src/redact.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { redactSecrets } from "./index"; + +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]"); + }); +}); diff --git a/src/cognitive/cli.py b/src/cognitive/cli.py index 2f2443b..047ba07 100644 --- a/src/cognitive/cli.py +++ b/src/cognitive/cli.py @@ -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(" Plan and execute a new mission") + print(" refine 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(" Plan and execute a mission") - print(" refine 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: diff --git a/src/cognitive/executor.py b/src/cognitive/executor.py index 1343686..25e2ab5 100644 --- a/src/cognitive/executor.py +++ b/src/cognitive/executor.py @@ -467,6 +467,7 @@ 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 @@ -474,6 +475,7 @@ def __init__( 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() @@ -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", @@ -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, diff --git a/src/cognitive/service.py b/src/cognitive/service.py index d6ff93d..b449b18 100644 --- a/src/cognitive/service.py +++ b/src/cognitive/service.py @@ -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") @@ -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, @@ -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)