feat: add secure workforce bundle installation

This commit is contained in:
4pmtong 2026-08-11 04:52:59 +08:00
parent 485c397b57
commit 6ffb54cd01
40 changed files with 5892 additions and 62 deletions

View file

@ -85,7 +85,11 @@ from app.utils.event_loop_utils import schedule_async_task_from_worker
from app.utils.server.sync_step import sync_step_event
from app.utils.workspace_paths import camel_log_root
from app.utils.workspace_resolver import get_workspace_resolver
from app.workspace_config import EffectiveEnvironmentSpec, canonical_digest
from app.workspace_config import (
EffectiveEnvironmentSpec,
WorkspaceBundleReconfigurationPendingError,
canonical_digest,
)
from app.workspace_config.admission import (
EnvironmentAdmissionService,
EnvironmentAdmissionTemplate,
@ -114,6 +118,17 @@ class _PreparedChatRun:
initial_action: ActionImproveData
def _workspace_bundle_admission_error(
exc: WorkspaceBundleReconfigurationPendingError,
) -> UserException:
return UserException(
code.error,
f"{exc}. Open Workspace configuration > Local setup and sync the "
"pending changes.",
error_code=exc.code,
)
_EXPLICIT_RESUME_INSTRUCTION = """
Resume the interrupted Run from its persisted Project context and durable
tool ledger. Continue only unfinished work. Treat completed tool calls and
@ -682,14 +697,17 @@ async def _prepare_chat_run(
environment = None
if isinstance(journal, SQLiteRunJournal):
template = _legacy_environment_template(data)
environment = await asyncio.to_thread(
EnvironmentAdmissionService(journal).persist_for_run,
run_id=run_context.run_id,
space_id=run_context.space_id,
working_directory=run_context.working_directory,
created_by=(run_context.user_id or "local-user"),
template=template,
)
try:
environment = await asyncio.to_thread(
EnvironmentAdmissionService(journal).persist_for_run,
run_id=run_context.run_id,
space_id=run_context.space_id,
working_directory=run_context.working_directory,
created_by=(run_context.user_id or "local-user"),
template=template,
)
except WorkspaceBundleReconfigurationPendingError as exc:
raise _workspace_bundle_admission_error(exc) from exc
_apply_environment_to_task_lock(
task_lock,
environment.spec,
@ -1270,14 +1288,17 @@ async def _improve_chat(
template,
EnvironmentAdmissionTemplate,
):
environment = await asyncio.to_thread(
EnvironmentAdmissionService(journal).persist_for_run,
run_id=refreshed_context.run_id,
space_id=refreshed_context.space_id,
working_directory=refreshed_context.working_directory,
created_by=(refreshed_context.user_id or "local-user"),
template=template,
)
try:
environment = await asyncio.to_thread(
EnvironmentAdmissionService(journal).persist_for_run,
run_id=refreshed_context.run_id,
space_id=refreshed_context.space_id,
working_directory=refreshed_context.working_directory,
created_by=(refreshed_context.user_id or "local-user"),
template=template,
)
except WorkspaceBundleReconfigurationPendingError as exc:
raise _workspace_bundle_admission_error(exc) from exc
_apply_environment_to_task_lock(
task_lock,
environment.spec,

View file

@ -8,10 +8,11 @@ from pathlib import Path
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, Header, HTTPException, Request
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
from app.auth import require_local_control_principal
from app.component.environment import env
from app.router_layer.hands_resolver import get_environment_hands
from app.run_journal import (
IdempotencyConflictError,
InvalidRunTransitionError,
@ -19,14 +20,16 @@ from app.run_journal import (
configured_run_journal_path,
get_default_run_journal,
)
from app.router_layer.hands_resolver import get_environment_hands
from app.utils.workspace_resolver import get_workspace_resolver
from app.workspace_bundle import (
HttpWorkspaceBundleCloudTransport,
WorkspaceBundleBindingsIncomplete,
WorkspaceBundleCloudError,
WorkspaceBundleInstallError,
WorkspaceBundleInstaller,
WorkspaceBundleInstallError,
WorkspaceSecretBroker,
WorkspaceSecretBrokerError,
WorkspaceSecretIdentity,
)
from app.workspace_config import ConfigPlacement
from app.workspace_git import ConfigurationRepositoryService
@ -78,6 +81,28 @@ class BundleMaterializeBody(BaseModel):
allow_content_repository_init: bool = False
class BundleLocalValueBinding(BaseModel):
model_config = ConfigDict(extra="forbid")
requirement_key: str = Field(min_length=1, max_length=1024)
requirement_kind: Literal["environment", "mcp_secret"]
secret_ref: str = Field(pattern=r"^wsvault_[A-Za-z0-9_-]{32}$")
account_scope_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
expected_binding_version: int | None = Field(default=None, ge=1)
class BundleLocalValuesBody(BaseModel):
model_config = ConfigDict(extra="forbid")
client_request_id: str = Field(min_length=1, max_length=200)
expected_version: int = Field(ge=0)
actor_id: str = Field(min_length=1, max_length=200)
bindings: list[BundleLocalValueBinding] = Field(
min_length=1,
max_length=100,
)
def _configuration_repository() -> ConfigurationRepositoryService:
journal = get_default_run_journal()
return ConfigurationRepositoryService(
@ -87,10 +112,15 @@ def _configuration_repository() -> ConfigurationRepositoryService:
def _installer(cloud=None) -> WorkspaceBundleInstaller:
try:
secret_broker = WorkspaceSecretBroker.from_environment()
except WorkspaceSecretBrokerError:
secret_broker = None
return WorkspaceBundleInstaller(
get_default_run_journal(),
_configuration_repository(),
cloud,
secret_broker,
)
@ -103,9 +133,7 @@ def _cloud(authorization: str) -> HttpWorkspaceBundleCloudTransport:
return HttpWorkspaceBundleCloudTransport(
server_url=server_url,
authorization=authorization,
desktop_instance_id=os.environ.get(
"EIGENT_DESKTOP_INSTANCE_ID", ""
),
desktop_instance_id=os.environ.get("EIGENT_DESKTOP_INSTANCE_ID", ""),
)
@ -117,14 +145,89 @@ def _payload(proposal_id: str) -> dict:
status_code=404,
detail={"code": "bundle_install_proposal_not_found"},
)
local_bindings = journal.list_workspace_bundle_local_bindings(proposal_id)
secret_bindings = journal.list_workspace_bundle_secret_bindings(
proposal_id
)
configured_values = {item.requirement_key for item in secret_bindings}
available_values: set[str] = set()
try:
broker = WorkspaceSecretBroker.from_environment()
except WorkspaceSecretBrokerError:
broker = None
if broker is not None and secret_bindings:
identities = tuple(
WorkspaceSecretIdentity(
secret_ref=item.secret_ref,
account_scope_digest=item.account_scope_digest,
space_id=proposal.space_id,
revision_id=proposal.revision_id,
slot_id=item.requirement_key,
)
for item in secret_bindings
)
try:
available_values.update(
verification.identity.slot_id
for verification in broker.verify_many(identities)
if verification.state == "available"
)
except WorkspaceSecretBrokerError:
# Availability is advisory in this payload, but must fail closed.
available_values.clear()
binding_versions = {
item.requirement_key: item.binding_version for item in secret_bindings
}
install_plan = proposal.install_plan
environment_requirements = install_plan.get("environment_requirements", [])
mcp_secret_requirements = install_plan.get("mcp_secret_requirements", [])
value_requirements = [
{
**item,
"requirement_kind": "environment",
"configured": item["requirement_key"] in configured_values,
"available": item["requirement_key"] in available_values,
"binding_version": binding_versions.get(item["requirement_key"]),
}
for item in environment_requirements
] + [
{
**item,
"requirement_kind": "mcp_secret",
"configured": item["requirement_key"] in configured_values,
"available": item["requirement_key"] in available_values,
"binding_version": binding_versions.get(item["requirement_key"]),
}
for item in mcp_secret_requirements
]
required = {
item["slot_id"] for item in install_plan.get("connector_slots", [])
}
required.update(install_plan.get("local_path_slots", []))
required.update(install_plan.get("script_actions", []))
required.update(
item["requirement_key"]
for item in environment_requirements
if item.get("required")
)
required.update(
item["requirement_key"] for item in mcp_secret_requirements
)
# Materialization verifies every binding, including optional values that
# the user chose to configure. Do not show Ready while such a binding is
# unreadable and would fail one step later.
required.update(configured_values)
configured = {item.slot_id for item in local_bindings}
configured.update(available_values)
missing = sorted(required - configured)
return {
"proposal": asdict(proposal),
"bindings": [
asdict(item)
for item in journal.list_workspace_bundle_local_bindings(
proposal_id
)
],
"bindings": [asdict(item) for item in local_bindings],
"value_requirements": value_requirements,
"readiness": {
"ready": not missing,
"missing_requirements": missing,
},
}
@ -214,6 +317,21 @@ async def get_bundle_install_proposal(proposal_id: str) -> dict:
return _payload(proposal_id)
@router.get("/spaces/{space_id}/workspace-bundle-installation")
async def get_space_bundle_installation(space_id: str) -> dict:
proposal = (
get_default_run_journal().get_latest_workspace_bundle_install_proposal(
space_id=space_id
)
)
if proposal is None:
raise HTTPException(
status_code=404,
detail={"code": "bundle_install_proposal_not_found"},
)
return _payload(proposal.proposal_id)
@router.post("/workspace-bundles/install-proposals/{proposal_id}/decision")
async def decide_bundle_install(
proposal_id: str, body: BundleDecisionBody
@ -287,9 +405,7 @@ async def approve_bundle_script(
raise _error(exc) from exc
@router.post(
"/workspace-bundles/install-proposals/{proposal_id}/materialize"
)
@router.post("/workspace-bundles/install-proposals/{proposal_id}/materialize")
async def materialize_bundle(
proposal_id: str,
body: BundleMaterializeBody,
@ -324,9 +440,7 @@ async def materialize_bundle(
expected_version=body.expected_version,
space_root=space_root,
actor_id=body.actor_id,
allow_content_repository_init=(
body.allow_content_repository_init
),
allow_content_repository_init=(body.allow_content_repository_init),
)
return _payload(proposal_id)
except Exception as exc:
@ -334,3 +448,37 @@ async def materialize_bundle(
finally:
if cloud is not None:
await cloud.close()
@router.put("/workspace-bundles/install-proposals/{proposal_id}/local-values")
async def bind_bundle_local_values(
proposal_id: str,
body: BundleLocalValuesBody,
) -> dict:
try:
journal = get_default_run_journal()
previous_refs = {
item.requirement_key: item.secret_ref
for item in journal.list_workspace_bundle_secret_bindings(
proposal_id
)
}
updated, _ = _installer().bind_local_values(
proposal_id,
client_request_id=body.client_request_id,
expected_version=body.expected_version,
bindings=[item.model_dump() for item in body.bindings],
authorized_by=body.actor_id,
)
payload = _payload(proposal_id)
payload["cleanup_secret_refs"] = sorted(
{
previous_refs[item.requirement_key]
for item in updated
if item.requirement_key in previous_refs
and previous_refs[item.requirement_key] != item.secret_ref
}
)
return payload
except Exception as exc:
raise _error(exc) from exc

View file

@ -14,9 +14,16 @@
class UserException(Exception):
def __init__(self, code: int, description: str):
def __init__(
self,
code: int,
description: str,
*,
error_code: str | None = None,
):
self.code = code
self.description = description
self.error_code = error_code
class TokenException(Exception):

View file

@ -59,7 +59,13 @@ async def token_exception(request: Request, e: TokenException):
async def user_exception(request: Request, e: UserException):
logger.info(f"User exception on {request.url.path}: {e.description}")
return JSONResponse(content={"code": e.code, "text": e.description})
payload: dict[str, int | str] = {
"code": e.code,
"text": e.description,
}
if e.error_code is not None:
payload["error_code"] = e.error_code
return JSONResponse(content=payload)
async def no_permission(request: Request, exception: NoPermissionException):

View file

@ -45,11 +45,12 @@ from app.run_journal.models import (
SpacePermissionProfileRevisionRecord,
StartupReconciliationResult,
ToolCallRecord,
WorkspaceBundleInstallProposalRecord,
WorkspaceBundleLocalBindingRecord,
WorkspaceBundleSecretBindingRecord,
WorkspaceConfigDraftRecord,
WorkspaceConfigMaterializationRecord,
WorkspaceConfigRevisionRecord,
WorkspaceBundleInstallProposalRecord,
WorkspaceBundleLocalBindingRecord,
WorkspaceOverlayEntryRecord,
WorkspaceReadSnapshotRecord,
WorkspaceSnapshotRangeRecord,
@ -122,6 +123,7 @@ __all__ = [
"WorkspaceConfigRevisionRecord",
"WorkspaceBundleInstallProposalRecord",
"WorkspaceBundleLocalBindingRecord",
"WorkspaceBundleSecretBindingRecord",
"WorkspaceOverlayEntryRecord",
"WorkspaceReadSnapshotRecord",
"WorkspaceSnapshotRangeRecord",

View file

@ -172,6 +172,20 @@ class WorkspaceBundleLocalBindingRecord:
authorized_at: float
@dataclass(frozen=True)
class WorkspaceBundleSecretBindingRecord:
binding_id: str
proposal_id: str
requirement_key: str
requirement_kind: str
binding_version: int
secret_ref: str
account_scope_digest: str
authorized_by: str
authorized_at: float
updated_at: float
@dataclass(frozen=True)
class EffectiveEnvironmentSpecRecord:
environment_spec_id: str

View file

@ -68,6 +68,7 @@ from app.run_journal.models import (
ToolCallRecord,
WorkspaceBundleInstallProposalRecord,
WorkspaceBundleLocalBindingRecord,
WorkspaceBundleSecretBindingRecord,
WorkspaceConfigDraftRecord,
WorkspaceConfigMaterializationRecord,
WorkspaceConfigRevisionRecord,
@ -98,7 +99,7 @@ from app.workspace_config.models import (
canonical_json,
)
SCHEMA_VERSION = 16
SCHEMA_VERSION = 17
logger = logging.getLogger("run_journal")
_MIGRATION_V1 = """
@ -1142,6 +1143,48 @@ PRAGMA user_version = 16;
COMMIT;
"""
_MIGRATION_V17 = """
BEGIN IMMEDIATE;
CREATE TABLE IF NOT EXISTS workspace_bundle_secret_bindings (
binding_id TEXT PRIMARY KEY,
proposal_id TEXT NOT NULL REFERENCES workspace_bundle_install_proposals(
proposal_id
) ON DELETE CASCADE,
requirement_key TEXT NOT NULL,
requirement_kind TEXT NOT NULL CHECK (
requirement_kind IN ('environment', 'mcp_secret')
),
binding_version INTEGER NOT NULL DEFAULT 1 CHECK (binding_version >= 1),
secret_ref TEXT NOT NULL,
account_scope_digest TEXT NOT NULL CHECK (
length(account_scope_digest) = 64
),
authorized_by TEXT NOT NULL,
authorized_at REAL NOT NULL,
updated_at REAL NOT NULL,
UNIQUE(proposal_id, requirement_key)
);
CREATE TABLE IF NOT EXISTS workspace_bundle_secret_binding_requests (
client_request_id TEXT PRIMARY KEY,
proposal_id TEXT NOT NULL REFERENCES workspace_bundle_install_proposals(
proposal_id
) ON DELETE CASCADE,
request_digest TEXT NOT NULL CHECK (length(request_digest) = 64),
created_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS workspace_bundle_secret_bindings_proposal_idx
ON workspace_bundle_secret_bindings(proposal_id, requirement_kind);
INSERT OR IGNORE INTO run_journal_migrations(version, applied_at)
VALUES (17, CAST(strftime('%s', 'now') AS REAL));
PRAGMA user_version = 17;
COMMIT;
"""
class RunJournalError(RuntimeError):
"""Base error for local RunJournal operations."""
@ -1759,9 +1802,7 @@ class SQLiteRunJournal:
# working content and move it to the next revision. The Cloud
# fact remains immutable at N while local edits continue at
# N+1 instead of being stranded behind a permanent conflict.
document["metadata"]["revision"] = (
receipt_revision_number + 1
)
document["metadata"]["revision"] = receipt_revision_number + 1
next_json = canonical_json(document)
updated = connection.execute(
"""
@ -1941,6 +1982,59 @@ class SQLiteRunJournal:
else None
)
def get_active_workspace_bundle_proposal(
self, *, space_id: str, revision_id: str
) -> WorkspaceBundleInstallProposalRecord | None:
"""Return the proposal that currently controls an installed revision.
Review-only proposals are deliberately excluded: creating a second
proposal must not disable a currently usable installation. Once an
approved proposal begins materialization, its state becomes an
admission gate until materialization completes.
"""
with self._lock:
row = self._connection.execute(
"""
SELECT * FROM workspace_bundle_install_proposals
WHERE space_id = ? AND revision_id = ?
AND state IN (
'materializing', 'materialized', 'needs_attention'
)
ORDER BY updated_at DESC, proposal_id DESC
LIMIT 1
""",
(space_id, revision_id),
).fetchone()
return (
self._workspace_bundle_install_proposal_from_row(row)
if row is not None
else None
)
def get_latest_workspace_bundle_install_proposal(
self,
*,
space_id: str,
) -> WorkspaceBundleInstallProposalRecord | None:
"""Return the durable install/setup flow currently owned by a Space."""
with self._lock:
row = self._connection.execute(
"""
SELECT * FROM workspace_bundle_install_proposals
WHERE space_id = ? AND state != 'rejected'
ORDER BY updated_at DESC, proposal_id DESC
LIMIT 1
""",
(space_id,),
).fetchone()
return (
self._workspace_bundle_install_proposal_from_row(row)
if row is not None
else None
)
def transition_workspace_bundle_install_proposal(
self,
proposal_id: str,
@ -2098,21 +2192,80 @@ class SQLiteRunJournal:
(proposal_id, slot_id),
).fetchone()
if row is not None:
actual = (
row["binding_id"],
row["proposal_id"],
row["slot_id"],
actual_resource = (
row["binding_kind"],
row["connector_id"],
row["opaque_connection_id"],
row["local_path"],
row["required_grants_json"],
row["authorized_by"],
)
if actual != expected:
raise IdempotencyConflictError(
f"Bundle slot {slot_id!r} already has another decision"
desired_resource = (
binding_kind,
connector_id,
opaque_connection_id,
local_path,
grants_json,
)
if actual_resource == desired_resource:
return (
self._workspace_bundle_local_binding_from_row(row),
self._workspace_bundle_install_proposal_from_row(
proposal
),
)
if int(proposal["version"]) != expected_proposal_version:
raise OptimisticConcurrencyError(
f"Bundle install proposal {proposal_id!r} changed"
)
if proposal["state"] not in {
"approved",
"needs_attention",
"materialized",
}:
raise InvalidRunTransitionError(
"Bundle resources can only be rebound after approval"
)
connection.execute(
"""UPDATE workspace_bundle_local_bindings
SET binding_kind = ?, connector_id = ?,
opaque_connection_id = ?, local_path = ?,
required_grants_json = ?, authorized_by = ?,
authorized_at = ?
WHERE binding_id = ?""",
(
binding_kind,
connector_id,
opaque_connection_id,
local_path,
grants_json,
authorized_by,
timestamp,
binding_id,
),
)
connection.execute(
"""UPDATE workspace_bundle_install_proposals
SET version = version + 1,
state = CASE WHEN state = 'materialized'
THEN 'needs_attention' ELSE state END,
error_code = CASE WHEN state = 'materialized'
THEN 'bundle_reconfiguration_pending'
ELSE error_code END,
updated_at = ?
WHERE proposal_id = ? AND version = ?""",
(timestamp, proposal_id, expected_proposal_version),
)
row = connection.execute(
"""SELECT * FROM workspace_bundle_local_bindings
WHERE binding_id = ?""",
(binding_id,),
).fetchone()
proposal = connection.execute(
"""SELECT * FROM workspace_bundle_install_proposals
WHERE proposal_id = ?""",
(proposal_id,),
).fetchone()
assert row is not None and proposal is not None
return (
self._workspace_bundle_local_binding_from_row(row),
self._workspace_bundle_install_proposal_from_row(proposal),
@ -2121,7 +2274,11 @@ class SQLiteRunJournal:
raise OptimisticConcurrencyError(
f"Bundle install proposal {proposal_id!r} changed"
)
if proposal["state"] not in {"approved", "needs_attention"}:
if proposal["state"] not in {
"approved",
"needs_attention",
"materialized",
}:
raise InvalidRunTransitionError(
"Bundle resources can only be bound after approval"
)
@ -2137,7 +2294,13 @@ class SQLiteRunJournal:
)
connection.execute(
"""UPDATE workspace_bundle_install_proposals
SET version = version + 1, updated_at = ?
SET version = version + 1,
state = CASE WHEN state = 'materialized'
THEN 'needs_attention' ELSE state END,
error_code = CASE WHEN state = 'materialized'
THEN 'bundle_reconfiguration_pending'
ELSE error_code END,
updated_at = ?
WHERE proposal_id = ? AND version = ?""",
(timestamp, proposal_id, expected_proposal_version),
)
@ -2171,6 +2334,277 @@ class SQLiteRunJournal:
for row in rows
)
def put_workspace_bundle_secret_bindings(
self,
*,
proposal_id: str,
client_request_id: str,
expected_proposal_version: int,
bindings: list[dict[str, Any]],
authorized_by: str,
now: float | None = None,
) -> tuple[
tuple[WorkspaceBundleSecretBindingRecord, ...],
WorkspaceBundleInstallProposalRecord,
]:
"""CAS opaque vault references without ever accepting secret values."""
if not client_request_id.strip() or not authorized_by.strip():
raise ValueError("binding request and authorizer are required")
if not bindings:
raise ValueError("at least one secret binding is required")
normalized: list[dict[str, Any]] = []
keys: set[str] = set()
allowed_ref_characters = (
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789_-"
)
for item in bindings:
requirement_key = str(item.get("requirement_key") or "")
requirement_kind = str(item.get("requirement_kind") or "")
secret_ref = str(item.get("secret_ref") or "")
account_scope_digest = str(item.get("account_scope_digest") or "")
expected_binding_version = item.get("expected_binding_version")
if expected_binding_version is not None:
expected_binding_version = int(expected_binding_version)
if expected_binding_version < 1:
raise ValueError("invalid Bundle secret binding version")
if requirement_kind not in {"environment", "mcp_secret"}:
raise ValueError("invalid Bundle secret binding kind")
if (
not requirement_key.strip()
or len(secret_ref) != 40
or not secret_ref.startswith("wsvault_")
or any(
character not in allowed_ref_characters
for character in secret_ref[8:]
)
or not self._is_sha256(account_scope_digest)
):
raise ValueError("invalid Bundle secret binding reference")
if requirement_key in keys:
raise ValueError("duplicate Bundle secret binding requirement")
keys.add(requirement_key)
normalized.append(
{
"requirement_key": requirement_key,
"requirement_kind": requirement_kind,
"secret_ref": secret_ref,
"account_scope_digest": account_scope_digest,
"expected_binding_version": expected_binding_version,
}
)
normalized.sort(key=lambda item: str(item["requirement_key"]))
request_digest = canonical_digest(
{
"proposal_id": proposal_id,
"bindings": normalized,
"authorized_by": authorized_by,
}
)
timestamp = now if now is not None else time.time()
with self._write_transaction() as connection:
receipt = connection.execute(
"""SELECT request_digest
FROM workspace_bundle_secret_binding_requests
WHERE client_request_id = ?""",
(client_request_id,),
).fetchone()
if receipt is not None:
if receipt["request_digest"] != request_digest:
raise IdempotencyConflictError(
"Bundle secret binding request id was reused"
)
rows = connection.execute(
f"""SELECT * FROM workspace_bundle_secret_bindings
WHERE proposal_id = ? AND requirement_key IN (
{",".join("?" for _ in normalized)}
) ORDER BY requirement_key""",
(
proposal_id,
*(item["requirement_key"] for item in normalized),
),
).fetchall()
proposal = connection.execute(
"""SELECT * FROM workspace_bundle_install_proposals
WHERE proposal_id = ?""",
(proposal_id,),
).fetchone()
if proposal is None:
raise RunNotFoundError(
f"Bundle install proposal {proposal_id!r} does not exist"
)
return (
tuple(
self._workspace_bundle_secret_binding_from_row(row)
for row in rows
),
self._workspace_bundle_install_proposal_from_row(proposal),
)
proposal = connection.execute(
"""SELECT * FROM workspace_bundle_install_proposals
WHERE proposal_id = ?""",
(proposal_id,),
).fetchone()
if proposal is None:
raise RunNotFoundError(
f"Bundle install proposal {proposal_id!r} does not exist"
)
if int(proposal["version"]) != expected_proposal_version:
raise OptimisticConcurrencyError(
f"Bundle install proposal {proposal_id!r} changed"
)
if proposal["state"] not in {
"approved",
"needs_attention",
"materialized",
}:
raise InvalidRunTransitionError(
"Bundle values can only be bound after approval"
)
changed = False
for item in normalized:
requirement_key = str(item["requirement_key"])
existing = connection.execute(
"""SELECT * FROM workspace_bundle_secret_bindings
WHERE proposal_id = ? AND requirement_key = ?""",
(proposal_id, requirement_key),
).fetchone()
expected_binding_version = item["expected_binding_version"]
if existing is None:
if expected_binding_version is not None:
raise OptimisticConcurrencyError(
f"Bundle value {requirement_key!r} changed"
)
binding_id = (
"bundlesecret_"
+ canonical_digest(
{
"proposal_id": proposal_id,
"requirement_key": requirement_key,
}
)[:32]
)
connection.execute(
"""INSERT INTO workspace_bundle_secret_bindings(
binding_id, proposal_id, requirement_key,
requirement_kind, binding_version, secret_ref,
account_scope_digest, authorized_by,
authorized_at, updated_at
) VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?)""",
(
binding_id,
proposal_id,
requirement_key,
item["requirement_kind"],
item["secret_ref"],
item["account_scope_digest"],
authorized_by,
timestamp,
timestamp,
),
)
changed = True
continue
if (
expected_binding_version is not None
and int(existing["binding_version"])
!= expected_binding_version
):
raise OptimisticConcurrencyError(
f"Bundle value {requirement_key!r} changed"
)
desired = (
item["requirement_kind"],
item["secret_ref"],
item["account_scope_digest"],
)
actual = (
existing["requirement_kind"],
existing["secret_ref"],
existing["account_scope_digest"],
)
if desired == actual:
continue
if expected_binding_version is None:
raise IdempotencyConflictError(
f"Bundle value {requirement_key!r} already exists"
)
connection.execute(
"""UPDATE workspace_bundle_secret_bindings
SET requirement_kind = ?, binding_version = binding_version + 1,
secret_ref = ?,
account_scope_digest = ?, authorized_by = ?,
authorized_at = ?, updated_at = ?
WHERE binding_id = ?""",
(
item["requirement_kind"],
item["secret_ref"],
item["account_scope_digest"],
authorized_by,
timestamp,
timestamp,
existing["binding_id"],
),
)
changed = True
connection.execute(
"""INSERT INTO workspace_bundle_secret_binding_requests(
client_request_id, proposal_id, request_digest, created_at
) VALUES (?, ?, ?, ?)""",
(
client_request_id,
proposal_id,
request_digest,
timestamp,
),
)
if changed:
connection.execute(
"""UPDATE workspace_bundle_install_proposals
SET version = version + 1, updated_at = ?
WHERE proposal_id = ? AND version = ?""",
(timestamp, proposal_id, expected_proposal_version),
)
rows = connection.execute(
f"""SELECT * FROM workspace_bundle_secret_bindings
WHERE proposal_id = ? AND requirement_key IN (
{",".join("?" for _ in normalized)}
) ORDER BY requirement_key""",
(
proposal_id,
*(item["requirement_key"] for item in normalized),
),
).fetchall()
proposal = connection.execute(
"""SELECT * FROM workspace_bundle_install_proposals
WHERE proposal_id = ?""",
(proposal_id,),
).fetchone()
assert proposal is not None
return (
tuple(
self._workspace_bundle_secret_binding_from_row(row)
for row in rows
),
self._workspace_bundle_install_proposal_from_row(proposal),
)
def list_workspace_bundle_secret_bindings(
self, proposal_id: str
) -> tuple[WorkspaceBundleSecretBindingRecord, ...]:
with self._lock:
rows = self._connection.execute(
"""SELECT * FROM workspace_bundle_secret_bindings
WHERE proposal_id = ? ORDER BY requirement_key""",
(proposal_id,),
).fetchall()
return tuple(
self._workspace_bundle_secret_binding_from_row(row)
for row in rows
)
def transition_workspace_config_revision(
self,
revision_id: str,
@ -9362,6 +9796,8 @@ class SQLiteRunJournal:
self._connection.executescript(_MIGRATION_V15)
if version < 16:
self._connection.executescript(_MIGRATION_V16)
if version < 17:
self._connection.executescript(_MIGRATION_V17)
@contextmanager
def _write_transaction(self) -> Iterator[sqlite3.Connection]:
@ -9711,6 +10147,23 @@ class SQLiteRunJournal:
authorized_at=float(row["authorized_at"]),
)
@staticmethod
def _workspace_bundle_secret_binding_from_row(
row: sqlite3.Row,
) -> WorkspaceBundleSecretBindingRecord:
return WorkspaceBundleSecretBindingRecord(
binding_id=row["binding_id"],
proposal_id=row["proposal_id"],
requirement_key=row["requirement_key"],
requirement_kind=row["requirement_kind"],
binding_version=int(row["binding_version"]),
secret_ref=row["secret_ref"],
account_scope_digest=row["account_scope_digest"],
authorized_by=row["authorized_by"],
authorized_at=float(row["authorized_at"]),
updated_at=float(row["updated_at"]),
)
@staticmethod
def _effective_environment_spec_from_row(
row: sqlite3.Row,

View file

@ -1,3 +1,4 @@
from app.workspace_bundle.authoring import WorkspaceBundleAuthoringService
from app.workspace_bundle.cloud import (
HttpWorkspaceBundleCloudTransport,
WorkspaceBundleCloudError,
@ -5,10 +6,15 @@ from app.workspace_bundle.cloud import (
)
from app.workspace_bundle.installer import (
WorkspaceBundleBindingsIncomplete,
WorkspaceBundleInstallError,
WorkspaceBundleInstaller,
WorkspaceBundleInstallError,
)
from app.workspace_bundle.secrets import (
WorkspaceSecretBroker,
WorkspaceSecretBrokerError,
WorkspaceSecretIdentity,
WorkspaceSecretVerification,
)
from app.workspace_bundle.authoring import WorkspaceBundleAuthoringService
__all__ = [
"HttpWorkspaceBundleCloudTransport",
@ -18,4 +24,8 @@ __all__ = [
"WorkspaceBundleInstallError",
"WorkspaceBundleInstaller",
"WorkspaceBundleAuthoringService",
"WorkspaceSecretBroker",
"WorkspaceSecretBrokerError",
"WorkspaceSecretIdentity",
"WorkspaceSecretVerification",
]

View file

@ -12,8 +12,14 @@ from app.run_journal import (
SQLiteRunJournal,
WorkspaceBundleInstallProposalRecord,
WorkspaceBundleLocalBindingRecord,
WorkspaceBundleSecretBindingRecord,
)
from app.workspace_bundle.cloud import WorkspaceBundleCloudTransport
from app.workspace_bundle.secrets import (
WorkspaceSecretBroker,
WorkspaceSecretBrokerError,
WorkspaceSecretIdentity,
)
from app.workspace_config import (
ConfigPlacement,
SecretValueInManifestError,
@ -51,10 +57,12 @@ class WorkspaceBundleInstaller:
journal: SQLiteRunJournal,
configuration_repository: ConfigurationRepositoryService,
cloud: WorkspaceBundleCloudTransport | None,
secret_broker: WorkspaceSecretBroker | None = None,
) -> None:
self.journal = journal
self.configuration_repository = configuration_repository
self.cloud = cloud
self.secret_broker = secret_broker
async def propose(
self,
@ -229,6 +237,91 @@ class WorkspaceBundleInstaller:
authorized_by=authorized_by,
)
def bind_local_values(
self,
proposal_id: str,
*,
client_request_id: str,
expected_version: int,
bindings: list[dict[str, Any]],
authorized_by: str,
) -> tuple[
tuple[WorkspaceBundleSecretBindingRecord, ...],
WorkspaceBundleInstallProposalRecord,
]:
proposal = self._proposal(proposal_id)
requirements = {
item["requirement_key"]: ("environment", item)
for item in proposal.install_plan.get(
"environment_requirements", []
)
}
requirements.update(
{
item["requirement_key"]: ("mcp_secret", item)
for item in proposal.install_plan.get(
"mcp_secret_requirements", []
)
}
)
if self.secret_broker is None:
raise WorkspaceBundleInstallError(
"Local secret storage is unavailable"
)
normalized: list[dict[str, Any]] = []
identities: list[WorkspaceSecretIdentity] = []
for binding in bindings:
requirement_key = str(binding.get("requirement_key") or "")
requirement_kind = str(binding.get("requirement_kind") or "")
requirement = requirements.get(requirement_key)
if requirement is None or requirement[0] != requirement_kind:
raise WorkspaceBundleInstallError(
"Local value does not match a declared Bundle requirement"
)
identity = WorkspaceSecretIdentity(
secret_ref=str(binding.get("secret_ref") or ""),
account_scope_digest=str(
binding.get("account_scope_digest") or ""
),
space_id=proposal.space_id,
revision_id=proposal.revision_id,
slot_id=requirement_key,
)
identities.append(identity)
normalized.append(
{
"requirement_key": requirement_key,
"requirement_kind": requirement_kind,
"secret_ref": identity.secret_ref,
"account_scope_digest": identity.account_scope_digest,
"expected_binding_version": binding.get(
"expected_binding_version"
),
}
)
try:
verifications = self.secret_broker.verify_many(identities)
except WorkspaceSecretBrokerError as exc:
raise WorkspaceBundleInstallError(
"Local values must be rebound"
) from exc
unavailable = [
verification.identity.slot_id
for verification in verifications
if verification.state != "available"
]
if unavailable:
raise WorkspaceBundleInstallError(
f"Local value {unavailable[0]!r} must be rebound"
)
return self.journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal_id,
client_request_id=client_request_id,
expected_proposal_version=expected_version,
bindings=normalized,
authorized_by=authorized_by,
)
async def materialize(
self,
proposal_id: str,
@ -250,7 +343,11 @@ class WorkspaceBundleInstaller:
bindings = self.journal.list_workspace_bundle_local_bindings(
proposal_id
)
self._require_complete_bindings(proposal, bindings)
secret_bindings = self.journal.list_workspace_bundle_secret_bindings(
proposal_id
)
self._require_complete_bindings(proposal, bindings, secret_bindings)
self._verify_secret_bindings(proposal, secret_bindings)
materializing = (
self.journal.transition_workspace_bundle_install_proposal(
proposal_id,
@ -565,6 +662,36 @@ class WorkspaceBundleInstaller:
}
),
"script_actions": sorted(script_actions),
"environment_requirements": [
{
"requirement_key": f"environment:{item.name}",
"name": item.name,
"required": item.required,
"sensitive": item.sensitive,
"description": item.description,
"example": item.example,
}
for item in (
manifest.spec.environment.variables
if manifest.spec.environment
else ()
)
],
"mcp_secret_requirements": sorted(
(
{
"requirement_key": (
f"mcp_secret:{server.id}:{slot_id}"
),
"mcp_id": server.id,
"slot_id": slot_id,
"required": True,
}
for server in manifest.spec.mcp_servers
for slot_id in server.secret_slots
),
key=lambda item: item["requirement_key"],
),
"permission_profile": manifest.spec.permissions.profile,
"git_policy": manifest.spec.git.model_dump(
by_alias=True, mode="json"
@ -578,6 +705,7 @@ class WorkspaceBundleInstaller:
def _require_complete_bindings(
proposal: WorkspaceBundleInstallProposalRecord,
bindings: tuple[WorkspaceBundleLocalBindingRecord, ...],
secret_bindings: tuple[WorkspaceBundleSecretBindingRecord, ...],
) -> None:
present = {binding.slot_id for binding in bindings}
required = {
@ -586,7 +714,58 @@ class WorkspaceBundleInstaller:
}
required.update(proposal.install_plan["local_path_slots"])
required.update(proposal.install_plan["script_actions"])
missing = sorted(required - present)
secret_present = {
binding.requirement_key for binding in secret_bindings
}
secret_required = {
item["requirement_key"]
for item in proposal.install_plan.get(
"environment_requirements", []
)
if item["required"]
}
secret_required.update(
item["requirement_key"]
for item in proposal.install_plan.get(
"mcp_secret_requirements", []
)
)
missing = sorted(
(required - present) | (secret_required - secret_present)
)
if missing:
raise WorkspaceBundleBindingsIncomplete(missing)
def _verify_secret_bindings(
self,
proposal: WorkspaceBundleInstallProposalRecord,
bindings: tuple[WorkspaceBundleSecretBindingRecord, ...],
) -> None:
if not bindings:
return
if self.secret_broker is None:
raise WorkspaceBundleBindingsIncomplete(
[binding.requirement_key for binding in bindings]
)
identities = tuple(
WorkspaceSecretIdentity(
secret_ref=binding.secret_ref,
account_scope_digest=binding.account_scope_digest,
space_id=proposal.space_id,
revision_id=proposal.revision_id,
slot_id=binding.requirement_key,
)
for binding in bindings
)
try:
verifications = self.secret_broker.verify_many(identities)
missing = [
verification.identity.slot_id
for verification in verifications
if verification.state != "available"
]
except WorkspaceSecretBrokerError:
missing = [identity.slot_id for identity in identities]
if missing:
raise WorkspaceBundleBindingsIncomplete(missing)

View file

@ -0,0 +1,258 @@
"""Capability-authenticated client for the Electron local secret broker."""
from __future__ import annotations
import json
import os
import re
from collections.abc import MutableMapping, Sequence
from dataclasses import dataclass
from http.client import HTTPConnection, HTTPException
from typing import Literal
from urllib.parse import urlsplit
def _capture_broker_environment(
environment: MutableMapping[str, str],
) -> tuple[str, str]:
"""Take broker authority out of the environment inherited by agents."""
endpoint = environment.pop("EIGENT_WORKSPACE_SECRET_BROKER_ENDPOINT", "")
capability = environment.pop(
"EIGENT_WORKSPACE_SECRET_BROKER_CAPABILITY", ""
)
legacy_endpoint = environment.pop(
"EIGENT_WORKFORCE_SECRET_BROKER_ENDPOINT", ""
)
legacy_capability = environment.pop(
"EIGENT_WORKFORCE_SECRET_BROKER_CAPABILITY", ""
)
if endpoint or capability:
return endpoint, capability
return legacy_endpoint, legacy_capability
_BROKER_ENDPOINT, _BROKER_CAPABILITY = _capture_broker_environment(os.environ)
class WorkspaceSecretBrokerError(RuntimeError):
"""Raised when a local-only Bundle binding cannot be verified."""
@dataclass(frozen=True)
class WorkspaceSecretIdentity:
secret_ref: str
account_scope_digest: str
space_id: str
revision_id: str
slot_id: str
@dataclass(frozen=True)
class WorkspaceSecretVerification:
identity: WorkspaceSecretIdentity
state: Literal["available", "missing", "needs_rebind"]
class WorkspaceSecretBroker:
"""Verify opaque bindings through Electron without receiving secret values."""
MAX_BATCH_BINDINGS = 100
MAX_REQUEST_BYTES = 16 * 1024
MAX_RESPONSE_BYTES = 64 * 1024
def __init__(
self,
*,
endpoint: str,
capability: str,
timeout_seconds: float = 3.0,
) -> None:
try:
parsed = urlsplit(endpoint)
except ValueError as exc:
raise WorkspaceSecretBrokerError(
"Workspace secret broker endpoint is invalid"
) from exc
if (
parsed.scheme != "http"
or parsed.hostname != "127.0.0.1"
or parsed.username is not None
or parsed.password is not None
or parsed.path not in {"", "/"}
or parsed.query
or parsed.fragment
):
raise WorkspaceSecretBrokerError(
"Workspace secret broker must use a loopback endpoint"
)
try:
port = parsed.port
except ValueError as exc:
raise WorkspaceSecretBrokerError(
"Workspace secret broker endpoint is invalid"
) from exc
if (
port is None
or not 1 <= port <= 65535
or re.fullmatch(r"[A-Za-z0-9_-]{32,256}", capability) is None
or not 0 < timeout_seconds <= 30
):
raise WorkspaceSecretBrokerError(
"Workspace secret broker configuration is invalid"
)
self._port = port
self._capability = capability
self._timeout_seconds = timeout_seconds
@classmethod
def from_environment(cls) -> WorkspaceSecretBroker:
if not _BROKER_ENDPOINT or not _BROKER_CAPABILITY:
raise WorkspaceSecretBrokerError(
"Workspace secret broker is unavailable"
)
return cls(
endpoint=_BROKER_ENDPOINT,
capability=_BROKER_CAPABILITY,
)
def verify(self, identity: WorkspaceSecretIdentity) -> None:
verification = self.verify_many((identity,))[0]
if verification.state != "available":
raise WorkspaceSecretBrokerError("Workspace secret is unavailable")
def verify_many(
self,
identities: Sequence[WorkspaceSecretIdentity],
) -> tuple[WorkspaceSecretVerification, ...]:
requested = tuple(identities)
if not requested:
return ()
if len(requested) > self.MAX_BATCH_BINDINGS:
raise WorkspaceSecretBrokerError(
"Workspace secret verification batch is too large"
)
response = self._request(
"/v1/workspace-secrets/verify-batch",
{
"bindings": [
self._identity_payload(identity) for identity in requested
]
},
)
if set(response) != {"statuses"}:
raise WorkspaceSecretBrokerError(
"Workspace secret broker returned an invalid verification"
)
statuses = response.get("statuses")
if not isinstance(statuses, list) or len(statuses) != len(requested):
raise WorkspaceSecretBrokerError(
"Workspace secret broker returned an invalid verification"
)
allowed_status_keys = {
"secret_ref",
"account_scope_digest",
"space_id",
"revision_id",
"slot_id",
"state",
"created_at",
"updated_at",
}
result: list[WorkspaceSecretVerification] = []
for identity, status in zip(requested, statuses, strict=True):
expected = self._identity_payload(identity)
if (
not isinstance(status, dict)
or not set(status).issubset(allowed_status_keys)
or any(
status.get(key) != value for key, value in expected.items()
)
or status.get("state")
not in {"available", "missing", "needs_rebind"}
):
raise WorkspaceSecretBrokerError(
"Workspace secret broker returned an invalid verification"
)
result.append(
WorkspaceSecretVerification(
identity=identity,
state=status["state"],
)
)
return tuple(result)
@staticmethod
def _identity_payload(identity: WorkspaceSecretIdentity) -> dict[str, str]:
return {
"secret_ref": identity.secret_ref,
"account_scope_digest": identity.account_scope_digest,
"space_id": identity.space_id,
"revision_id": identity.revision_id,
"slot_id": identity.slot_id,
}
def _request(self, path: str, request: dict) -> dict:
encoded = json.dumps(
request, separators=(",", ":"), sort_keys=True
).encode("utf-8")
if len(encoded) > self.MAX_REQUEST_BYTES:
raise WorkspaceSecretBrokerError(
"Workspace secret broker request is too large"
)
connection = HTTPConnection(
"127.0.0.1",
self._port,
timeout=self._timeout_seconds,
)
try:
connection.request(
"POST",
path,
body=encoded,
headers={
"Authorization": f"Bearer {self._capability}",
"Content-Type": "application/json",
"Accept": "application/json",
},
)
http_response = connection.getresponse()
content_length = http_response.getheader("Content-Length")
if content_length is not None:
try:
declared_length = int(content_length)
except ValueError as exc:
raise WorkspaceSecretBrokerError(
"Workspace secret broker returned invalid metadata"
) from exc
if declared_length > self.MAX_RESPONSE_BYTES:
raise WorkspaceSecretBrokerError(
"Workspace secret broker response is too large"
)
raw = http_response.read(self.MAX_RESPONSE_BYTES + 1)
if len(raw) > self.MAX_RESPONSE_BYTES:
raise WorkspaceSecretBrokerError(
"Workspace secret broker response is too large"
)
if http_response.status != 200:
raise WorkspaceSecretBrokerError(
"Workspace secret is unavailable "
f"(broker_status_{http_response.status})"
)
except (OSError, TimeoutError, HTTPException) as exc:
raise WorkspaceSecretBrokerError(
"Workspace secret broker is unavailable"
) from exc
finally:
connection.close()
try:
response = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise WorkspaceSecretBrokerError(
"Workspace secret broker returned an invalid response"
) from exc
if not isinstance(response, dict):
raise WorkspaceSecretBrokerError(
"Workspace secret broker returned an invalid response"
)
return response

View file

@ -32,6 +32,7 @@ from app.workspace_config.models import (
UnsafeCloudProjectionError,
UnsupportedThinkingEffortError,
WorkforceBundleManifest,
WorkspaceBundleReconfigurationPendingError,
WorkspaceConfigError,
WorkspaceLock,
WorktreeMaterialization,
@ -60,6 +61,7 @@ __all__ = [
"UnsafeCloudProjectionError",
"UnsupportedThinkingEffortError",
"WorkforceBundleManifest",
"WorkspaceBundleReconfigurationPendingError",
"WorkspaceLock",
"WorkspaceConfigError",
"WorktreeMaterialization",

View file

@ -36,6 +36,7 @@ from app.workspace_config.models import (
ResolvedContextSource,
ThinkingEffort,
WorkforceBundleManifest,
WorkspaceBundleReconfigurationPendingError,
canonical_digest,
normalize_thinking_effort,
)
@ -300,10 +301,15 @@ class EnvironmentAdmissionService:
installed_manifest = WorkforceBundleManifest.model_validate(
revision.manifest
)
proposal = self.journal.get_materialized_workspace_bundle_proposal(
proposal = self.journal.get_active_workspace_bundle_proposal(
space_id=space_id,
revision_id=installed.revision_id,
)
if proposal is not None and proposal.state != "materialized":
raise WorkspaceBundleReconfigurationPendingError(
proposal_id=proposal.proposal_id,
state=proposal.state,
)
if proposal is not None:
bindings = {
item.slot_id: item

View file

@ -52,6 +52,20 @@ class UnsafeCloudProjectionError(WorkspaceConfigError):
"""Raised when a Cloud projection contains device-local identity."""
class WorkspaceBundleReconfigurationPendingError(WorkspaceConfigError):
"""Raised when an installed Bundle must be re-synced before Run admission."""
code = "workspace_bundle_reconfiguration_pending"
def __init__(self, *, proposal_id: str, state: str) -> None:
self.proposal_id = proposal_id
self.state = state
super().__init__(
"Workspace Bundle local setup changed and must be synced before "
"starting another Run"
)
class ThinkingEffort(StrEnum):
LOW = "low"
MEDIUM = "medium"

View file

@ -0,0 +1,357 @@
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.controller import workspace_bundle_controller
from app.run_journal import SQLiteRunJournal
from app.workspace_bundle import WorkspaceSecretVerification
def test_electron_generated_secret_reference_matches_brain_contract():
electron_generated_ref = f"wsvault_{'A' * 32}"
binding = (
workspace_bundle_controller.BundleLocalValueBinding.model_validate(
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": electron_generated_ref,
"account_scope_digest": "a" * 64,
}
)
)
assert binding.secret_ref == electron_generated_ref
def test_brain_rejects_noncanonical_vault_references():
with pytest.raises(ValidationError):
workspace_bundle_controller.BundleLocalValueBinding.model_validate(
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": f"wsvault_{'A' * 31}",
"account_scope_digest": "a" * 64,
}
)
def test_install_payload_masks_vault_references(tmp_path, monkeypatch):
journal = SQLiteRunJournal(tmp_path / "run-journal.sqlite3")
monkeypatch.setattr(
workspace_bundle_controller,
"get_default_run_journal",
lambda: journal,
)
proposal = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-1",
request_id="proposal-request-1",
space_id="space-1",
bundle_id="bundle-1",
revision_id="bundle-1@1",
config_placement="sidecar",
manifest={"spec": {}},
assets=[],
install_plan={
"connector_slots": [],
"local_path_slots": [],
"script_actions": [],
"environment_requirements": [
{
"requirement_key": "environment:API_TOKEN",
"name": "API_TOKEN",
"required": True,
"sensitive": True,
"description": "API token",
"example": None,
}
],
"mcp_secret_requirements": [],
},
)
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="approved",
decided_by="user-1",
)
# Electron emits this exact opaque format: prefix plus 32 base64url chars.
secret_ref = f"wsvault_{'A' * 32}"
journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="binding-request-1",
expected_proposal_version=proposal.version,
bindings=[
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": secret_ref,
"account_scope_digest": "a" * 64,
}
],
authorized_by="user-1",
)
class AvailableBroker:
def __init__(self):
self.batches = []
def verify_many(self, identities):
self.batches.append(tuple(identities))
return tuple(
WorkspaceSecretVerification(
identity=identity,
state="available",
)
for identity in identities
)
available_broker = AvailableBroker()
monkeypatch.setattr(
workspace_bundle_controller.WorkspaceSecretBroker,
"from_environment",
lambda: available_broker,
)
payload = workspace_bundle_controller._payload(proposal.proposal_id)
assert payload["readiness"] == {
"ready": True,
"missing_requirements": [],
}
assert payload["value_requirements"][0]["configured"] is True
assert payload["value_requirements"][0]["available"] is True
assert payload["value_requirements"][0]["binding_version"] == 1
assert secret_ref not in repr(payload)
assert "account_scope_digest" not in repr(payload)
assert len(available_broker.batches) == 1
assert available_broker.batches[0][0].secret_ref == secret_ref
journal.close()
def test_install_payload_marks_missing_vault_value_unready(
tmp_path, monkeypatch
):
journal = SQLiteRunJournal(tmp_path / "run-journal.sqlite3")
monkeypatch.setattr(
workspace_bundle_controller,
"get_default_run_journal",
lambda: journal,
)
proposal = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-missing",
request_id="proposal-request-missing",
space_id="space-1",
bundle_id="bundle-1",
revision_id="bundle-1@1",
config_placement="sidecar",
manifest={"spec": {}},
assets=[],
install_plan={
"connector_slots": [],
"local_path_slots": [],
"script_actions": [],
"environment_requirements": [
{
"requirement_key": "environment:API_TOKEN",
"name": "API_TOKEN",
"required": True,
}
],
"mcp_secret_requirements": [],
},
)
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="approved",
decided_by="user-1",
)
journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="binding-request-missing",
expected_proposal_version=proposal.version,
bindings=[
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": f"wsvault_{'M' * 32}",
"account_scope_digest": "a" * 64,
}
],
authorized_by="user-1",
)
payload = workspace_bundle_controller._payload(proposal.proposal_id)
assert payload["readiness"] == {
"ready": False,
"missing_requirements": ["environment:API_TOKEN"],
}
assert payload["value_requirements"][0]["configured"] is True
assert payload["value_requirements"][0]["available"] is False
journal.close()
def test_local_value_contract_rejects_plaintext_fields():
with pytest.raises(ValidationError):
workspace_bundle_controller.BundleLocalValuesBody.model_validate(
{
"client_request_id": "request-1",
"expected_version": 1,
"actor_id": "user-1",
"bindings": [
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": f"wsvault_{'P' * 32}",
"account_scope_digest": "a" * 64,
"value": "plaintext-must-never-enter-brain",
}
],
}
)
def test_space_installation_lookup_resumes_latest_non_rejected_proposal(
tmp_path,
monkeypatch,
):
journal = SQLiteRunJournal(tmp_path / "run-journal.sqlite3")
monkeypatch.setattr(
workspace_bundle_controller,
"get_default_run_journal",
lambda: journal,
)
rejected = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-rejected",
request_id="request-rejected",
space_id="space-1",
bundle_id="bundle-old",
revision_id="bundle-old@1",
config_placement="sidecar",
manifest={"spec": {}},
assets=[],
install_plan={},
now=1,
)
journal.transition_workspace_bundle_install_proposal(
rejected.proposal_id,
expected_version=rejected.version,
state="rejected",
decided_by="user-1",
now=2,
)
active = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-active",
request_id="request-active",
space_id="space-1",
bundle_id="bundle-current",
revision_id="bundle-current@2",
config_placement="sidecar",
manifest={"spec": {}},
assets=[],
install_plan={},
now=3,
)
found = journal.get_latest_workspace_bundle_install_proposal(
space_id="space-1"
)
assert found is not None
assert found.proposal_id == active.proposal_id
journal.close()
@pytest.mark.asyncio
async def test_local_value_put_returns_only_the_exact_ref_replaced_by_cas(
tmp_path,
monkeypatch,
):
journal = SQLiteRunJournal(tmp_path / "run-journal.sqlite3")
monkeypatch.setattr(
workspace_bundle_controller,
"get_default_run_journal",
lambda: journal,
)
proposal = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-cleanup",
request_id="proposal-cleanup-request",
space_id="space-1",
bundle_id="bundle-1",
revision_id="bundle-1@1",
config_placement="sidecar",
manifest={"spec": {}},
assets=[],
install_plan={
"connector_slots": [],
"local_path_slots": [],
"script_actions": [],
"environment_requirements": [],
"mcp_secret_requirements": [],
},
)
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="approved",
decided_by="user-1",
)
old_ref = f"wsvault_{'O' * 32}"
stored, proposal = journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="old-binding",
expected_proposal_version=proposal.version,
bindings=[
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": old_ref,
"account_scope_digest": "a" * 64,
}
],
authorized_by="user-1",
)
new_ref = f"wsvault_{'N' * 32}"
class Installer:
def bind_local_values(self, proposal_id, **kwargs):
return journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal_id,
client_request_id=kwargs["client_request_id"],
expected_proposal_version=kwargs["expected_version"],
bindings=kwargs["bindings"],
authorized_by=kwargs["authorized_by"],
)
monkeypatch.setattr(
workspace_bundle_controller,
"_installer",
lambda: Installer(),
)
response = await workspace_bundle_controller.bind_bundle_local_values(
proposal.proposal_id,
workspace_bundle_controller.BundleLocalValuesBody.model_validate(
{
"client_request_id": "replace-binding",
"expected_version": proposal.version,
"actor_id": "user-1",
"bindings": [
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": new_ref,
"account_scope_digest": "a" * 64,
"expected_binding_version": stored[0].binding_version,
}
],
}
),
)
assert response["cleanup_secret_refs"] == [old_ref]
assert new_ref not in response["cleanup_secret_refs"]
journal.close()

View file

@ -832,6 +832,208 @@ def test_database_reopens_without_reapplying_or_losing_migration(tmp_path):
assert reopened.get_run("run-1") is not None
def test_bundle_secret_bindings_persist_only_opaque_refs_and_replay_requests(
tmp_path,
):
path = tmp_path / "run-journal.sqlite3"
sentinel = "raw-secret-must-never-enter-journal"
with SQLiteRunJournal(path) as journal:
proposal = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-1",
request_id="proposal-request-1",
space_id="space-1",
bundle_id="bundle-1",
revision_id="bundle-1@1",
config_placement="sidecar",
manifest={"spec": {}},
assets=[],
install_plan={},
)
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="approved",
decided_by="user-1",
)
first_ref = f"wsvault_{'A' * 32}"
second_ref = f"wsvault_{'B' * 32}"
bindings = [
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": first_ref,
"account_scope_digest": "a" * 64,
}
]
stored, advanced = journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="bind-request-1",
expected_proposal_version=proposal.version,
bindings=bindings,
authorized_by="user-1",
)
replay, replay_proposal = journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="bind-request-1",
expected_proposal_version=proposal.version,
bindings=bindings,
authorized_by="user-1",
)
assert replay == stored
assert replay_proposal == advanced
assert stored[0].secret_ref == first_ref
assert stored[0].binding_version == 1
with pytest.raises(IdempotencyConflictError):
journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="bind-request-1",
expected_proposal_version=advanced.version,
bindings=[
{
**bindings[0],
"secret_ref": f"wsvault_{'C' * 32}",
}
],
authorized_by="user-1",
)
replaced, replaced_proposal = (
journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="bind-request-2",
expected_proposal_version=advanced.version,
bindings=[
{
**bindings[0],
"secret_ref": second_ref,
"expected_binding_version": 1,
}
],
authorized_by="user-1",
)
)
assert replaced[0].binding_version == 2
assert replaced[0].secret_ref == second_ref
assert replaced_proposal.version == advanced.version + 1
with pytest.raises(OptimisticConcurrencyError):
journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="bind-request-stale",
expected_proposal_version=replaced_proposal.version,
bindings=[
{
**bindings[0],
"secret_ref": f"wsvault_{'D' * 32}",
"expected_binding_version": 1,
}
],
authorized_by="user-1",
)
database_bytes = path.read_bytes()
wal = path.with_name(path.name + "-wal")
if wal.exists():
database_bytes += wal.read_bytes()
assert sentinel.encode() not in database_bytes
def test_materialized_bundle_bindings_can_be_reconfigured_with_cas(tmp_path):
with SQLiteRunJournal(tmp_path / "run-journal.sqlite3") as journal:
proposal = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-installed",
request_id="proposal-installed-request",
space_id="space-1",
bundle_id="bundle-1",
revision_id="bundle-1@1",
config_placement="sidecar",
manifest={"spec": {}},
assets=[],
install_plan={},
)
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="approved",
decided_by="user-1",
)
_, proposal = journal.put_workspace_bundle_local_binding(
proposal_id=proposal.proposal_id,
expected_proposal_version=proposal.version,
slot_id="docs",
binding_kind="local_path",
connector_id=None,
opaque_connection_id=None,
local_path="/first/docs",
required_grants=[],
authorized_by="user-1",
)
secret_bindings, proposal = (
journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="secret-first",
expected_proposal_version=proposal.version,
bindings=[
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": f"wsvault_{'A' * 32}",
"account_scope_digest": "a" * 64,
}
],
authorized_by="user-1",
)
)
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="materializing",
)
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="materialized",
)
rebound_path, proposal = journal.put_workspace_bundle_local_binding(
proposal_id=proposal.proposal_id,
expected_proposal_version=proposal.version,
slot_id="docs",
binding_kind="local_path",
connector_id=None,
opaque_connection_id=None,
local_path="/replacement/docs",
required_grants=[],
authorized_by="user-2",
)
rebound_secret, proposal = (
journal.put_workspace_bundle_secret_bindings(
proposal_id=proposal.proposal_id,
client_request_id="secret-replacement",
expected_proposal_version=proposal.version,
bindings=[
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": f"wsvault_{'B' * 32}",
"account_scope_digest": "a" * 64,
"expected_binding_version": secret_bindings[
0
].binding_version,
}
],
authorized_by="user-2",
)
)
assert proposal.state == "needs_attention"
assert proposal.error_code == "bundle_reconfiguration_pending"
assert rebound_path.local_path == "/replacement/docs"
assert rebound_path.authorized_by == "user-2"
assert rebound_secret[0].binding_version == 2
assert rebound_secret[0].secret_ref == f"wsvault_{'B' * 32}"
def test_v14_database_backfills_finite_expiry_for_pending_approval(tmp_path):
path = tmp_path / "run-journal.sqlite3"
with SQLiteRunJournal(path) as current:

View file

@ -43,6 +43,29 @@ def test_user_exception_returns_api_error_instead_of_http_500():
}
def test_user_exception_can_return_a_stable_typed_error_code():
test_api = FastAPI()
register_exception_handlers(test_api)
@test_api.get("/workspace-bundle-pending")
async def workspace_bundle_pending():
raise UserException(
1,
"Sync local setup before starting another Run",
error_code="workspace_bundle_reconfiguration_pending",
)
with TestClient(test_api) as client:
response = client.get("/workspace-bundle-pending")
assert response.status_code == 200
assert response.json() == {
"code": 1,
"text": "Sync local setup before starting another Run",
"error_code": "workspace_bundle_reconfiguration_pending",
}
def test_validation_handler_works_without_eager_translation_import():
test_api = FastAPI()
register_exception_handlers(test_api)

View file

@ -10,6 +10,7 @@ from app.workspace_bundle import (
WorkspaceBundleBindingsIncomplete,
WorkspaceBundleInstaller,
WorkspaceBundleInstallError,
WorkspaceSecretVerification,
)
from app.workspace_config import (
ConfigPlacement,
@ -239,6 +240,29 @@ class FakeCloud:
return None
class FakeSecretBroker:
def __init__(self):
self.verified = []
self.batches = []
self.rejected: set[str] = set()
def verify_many(self, identities):
batch = tuple(identities)
self.batches.append(batch)
self.verified.extend(batch)
return tuple(
WorkspaceSecretVerification(
identity=identity,
state=(
"needs_rebind"
if identity.secret_ref in self.rejected
else "available"
),
)
for identity in batch
)
@pytest.fixture
def installer(tmp_path):
journal = SQLiteRunJournal(tmp_path / "journal.sqlite3")
@ -518,6 +542,256 @@ async def test_materialize_rejects_secret_bearing_downloaded_script(installer):
assert not (config_root / "skills/research.py").exists()
@pytest.mark.asyncio
async def test_required_local_values_block_before_cloud_and_optional_env_does_not(
installer,
):
service, journal, cloud, tmp_path = installer
manifest = _manifest()
manifest["spec"].update(
{
"instructions": {},
"context": [],
"skills": [],
"connectors": [],
"environment": {
"variables": [
{
"name": "API_TOKEN",
"required": True,
"sensitive": True,
},
{
"name": "LOG_LEVEL",
"required": False,
"sensitive": False,
"example": "info",
},
]
},
"mcpServers": [
{
"id": "linear",
"definition": "registry://mcp/linear@1",
"secretSlots": ["LINEAR_API_TOKEN"],
"assignTo": ["lead"],
}
],
}
)
async def get_revision(bundle_id, revision_id):
canonical = WorkforceBundleManifest.model_validate(
manifest
).canonical_payload()
return {
"id": revision_id,
"bundle_id": bundle_id,
"status": "published",
"manifest": canonical,
"manifest_digest": canonical_digest(canonical),
"assets": [],
}
cloud.get_revision = get_revision
broker = FakeSecretBroker()
service.secret_broker = broker
proposal = await service.propose(
proposal_id="proposal-values",
request_id="request-values",
space_id="space-1",
bundle_id="bundle-research",
revision_id="bundle-research@1",
config_placement=ConfigPlacement.SIDECAR,
)
assert proposal.install_plan["environment_requirements"] == [
{
"requirement_key": "environment:API_TOKEN",
"name": "API_TOKEN",
"required": True,
"sensitive": True,
"description": None,
"example": None,
},
{
"requirement_key": "environment:LOG_LEVEL",
"name": "LOG_LEVEL",
"required": False,
"sensitive": False,
"description": None,
"example": "info",
},
]
assert proposal.install_plan["mcp_secret_requirements"] == [
{
"requirement_key": "mcp_secret:linear:LINEAR_API_TOKEN",
"mcp_id": "linear",
"slot_id": "LINEAR_API_TOKEN",
"required": True,
}
]
proposal = service.decide(
proposal.proposal_id,
expected_version=proposal.version,
approved=True,
decided_by="user-1",
)
with pytest.raises(WorkspaceBundleBindingsIncomplete) as missing:
await service.materialize(
proposal.proposal_id,
expected_version=proposal.version,
space_root=tmp_path,
actor_id="user-1",
)
assert set(missing.value.missing_slots) == {
"environment:API_TOKEN",
"mcp.server.start:linear",
"mcp_secret:linear:LINEAR_API_TOKEN",
}
assert cloud.installed_bundle_id is None
_, proposal = service.approve_script_action(
proposal.proposal_id,
expected_version=proposal.version,
action_id="mcp.server.start:linear",
authorized_by="user-1",
)
_, proposal = service.bind_local_values(
proposal.proposal_id,
client_request_id="bind-values-1",
expected_version=proposal.version,
authorized_by="user-1",
bindings=[
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": f"wsvault_{'E' * 32}",
"account_scope_digest": "a" * 64,
},
{
"requirement_key": "mcp_secret:linear:LINEAR_API_TOKEN",
"requirement_kind": "mcp_secret",
"secret_ref": f"wsvault_{'M' * 32}",
"account_scope_digest": "a" * 64,
},
],
)
result = await service.materialize(
proposal.proposal_id,
expected_version=proposal.version,
space_root=tmp_path,
actor_id="user-1",
)
assert result.state == "materialized"
assert {item.slot_id for item in broker.verified} == {
"environment:API_TOKEN",
"mcp_secret:linear:LINEAR_API_TOKEN",
}
assert [len(batch) for batch in broker.batches] == [2, 2]
@pytest.mark.asyncio
async def test_unreadable_bound_value_blocks_all_cloud_side_effects(installer):
service, _, cloud, tmp_path = installer
manifest = _manifest()
manifest["spec"]["context"] = []
manifest["spec"]["skills"] = []
manifest["spec"]["connectors"] = []
manifest["spec"]["environment"] = {
"variables": [{"name": "API_TOKEN", "required": True}]
}
async def get_revision(bundle_id, revision_id):
canonical = WorkforceBundleManifest.model_validate(
manifest
).canonical_payload()
return {
"id": revision_id,
"bundle_id": bundle_id,
"status": "published",
"manifest": canonical,
"manifest_digest": canonical_digest(canonical),
"assets": [],
}
cloud.get_revision = get_revision
broker = FakeSecretBroker()
service.secret_broker = broker
proposal = await service.propose(
proposal_id="proposal-unreadable",
request_id="request-unreadable",
space_id="space-1",
bundle_id="bundle-research",
revision_id="bundle-research@1",
config_placement=ConfigPlacement.SIDECAR,
)
proposal = service.decide(
proposal.proposal_id,
expected_version=proposal.version,
approved=True,
decided_by="user-1",
)
_, proposal = service.bind_local_values(
proposal.proposal_id,
client_request_id="bind-unreadable",
expected_version=proposal.version,
authorized_by="user-1",
bindings=[
{
"requirement_key": "environment:API_TOKEN",
"requirement_kind": "environment",
"secret_ref": f"wsvault_{'U' * 32}",
"account_scope_digest": "a" * 64,
}
],
)
broker.rejected.add(f"wsvault_{'U' * 32}")
with pytest.raises(WorkspaceBundleBindingsIncomplete):
await service.materialize(
proposal.proposal_id,
expected_version=proposal.version,
space_root=tmp_path,
actor_id="user-1",
)
assert cloud.installed_bundle_id is None
assert cloud.projection is None
assert [len(batch) for batch in broker.batches] == [1, 1]
def test_legacy_install_plan_without_local_value_requirements_is_supported(
installer,
):
service, journal, _, _ = installer
proposal = journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-legacy-plan",
request_id="request-legacy-plan",
space_id="space-1",
bundle_id="bundle-research",
revision_id="bundle-research@1",
config_placement="sidecar",
manifest=_manifest(),
assets=[],
install_plan={
"connector_slots": [],
"local_path_slots": [],
"script_actions": [],
},
)
proposal = service.decide(
proposal.proposal_id,
expected_version=proposal.version,
approved=True,
decided_by="user-1",
)
service._require_complete_bindings(proposal, (), ())
@pytest.mark.asyncio
async def test_projection_response_loss_retries_without_duplicate_grants(
tmp_path,
@ -634,6 +908,52 @@ async def test_upgrade_reviews_again_and_commits_new_configuration(installer):
)
@pytest.mark.asyncio
async def test_materialized_local_rebind_requires_and_completes_cloud_resync(
installer,
):
service, journal, cloud, tmp_path = installer
proposal = await _approved_and_bound(service, journal, tmp_path)
installed = await service.materialize(
proposal.proposal_id,
expected_version=proposal.version,
space_root=tmp_path,
actor_id="user-1",
)
first_projection_id = cloud.projection["projection_id"]
replacement = tmp_path / "replacement-docs"
replacement.mkdir()
_, pending = service.bind_connector(
installed.proposal_id,
expected_version=installed.version,
slot_id="github_readonly",
connector_id="github",
opaque_connection_id="connection-2",
authorized_by="user-1",
)
_, pending = service.bind_local_path(
installed.proposal_id,
expected_version=pending.version,
slot_id="docs_folder",
local_path=replacement,
authorized_by="user-1",
)
assert pending.state == "needs_attention"
assert pending.error_code == "bundle_reconfiguration_pending"
resynced = await service.materialize(
pending.proposal_id,
expected_version=pending.version,
space_root=tmp_path,
actor_id="user-1",
)
assert resynced.state == "materialized"
assert cloud.projection["projection_id"] != first_projection_id
assert len(cloud.projections) == 2
assert cloud.binding == ("github_readonly", "connection-2")
def test_startup_reconciliation_exposes_interrupted_materialization(tmp_path):
path = tmp_path / "journal.sqlite3"
with SQLiteRunJournal(path) as journal:

View file

@ -0,0 +1,247 @@
from __future__ import annotations
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import pytest
from app.workspace_bundle import (
WorkspaceSecretBroker,
WorkspaceSecretBrokerError,
WorkspaceSecretIdentity,
)
from app.workspace_bundle.secrets import _capture_broker_environment
def _serve_once(
response: dict, *, status_code: int = 200
) -> tuple[str, list[dict], threading.Thread]:
requests: list[dict] = []
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
body = json.loads(self.rfile.read(length))
requests.append(
{
"path": self.path,
"authorization": self.headers.get("Authorization"),
"content_type": self.headers.get("Content-Type"),
"body": body,
}
)
encoded = json.dumps(response).encode()
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def log_message(self, format, *args):
return
server = HTTPServer(("127.0.0.1", 0), Handler)
port = server.server_port
def serve() -> None:
try:
server.handle_request()
finally:
server.server_close()
thread = threading.Thread(target=serve)
thread.start()
return f"http://127.0.0.1:{port}", requests, thread
def _identity() -> WorkspaceSecretIdentity:
return WorkspaceSecretIdentity(
secret_ref="wsvault_example-reference",
account_scope_digest="a" * 64,
space_id="space-1",
revision_id="bundle-1@1",
slot_id="environment:API_TOKEN",
)
def test_secret_broker_binds_capability_and_full_identity():
identity = _identity()
endpoint, requests, thread = _serve_once(
{
"statuses": [
{
**identity.__dict__,
"state": "available",
}
]
}
)
broker = WorkspaceSecretBroker(
endpoint=endpoint,
capability="x" * 43,
)
broker.verify(identity)
thread.join(timeout=2)
assert requests == [
{
"path": "/v1/workspace-secrets/verify-batch",
"authorization": "Bearer " + "x" * 43,
"content_type": "application/json",
"body": {
"bindings": [
{
"secret_ref": "wsvault_example-reference",
"account_scope_digest": "a" * 64,
"space_id": "space-1",
"revision_id": "bundle-1@1",
"slot_id": "environment:API_TOKEN",
}
]
},
}
]
def test_secret_broker_fails_closed_without_returning_error_payload():
sentinel = "must-not-appear-in-error"
endpoint, _, thread = _serve_once(
{"error_code": "scope_mismatch", "value": sentinel},
status_code=403,
)
broker = WorkspaceSecretBroker(
endpoint=endpoint,
capability="x" * 43,
)
with pytest.raises(WorkspaceSecretBrokerError) as caught:
broker.verify(_identity())
thread.join(timeout=2)
assert sentinel not in str(caught.value)
assert not hasattr(broker, "resolve")
@pytest.mark.parametrize(
"endpoint",
[
"http://0.0.0.0:1234",
"https://127.0.0.1:1234",
"http://localhost:1234",
"http://127.0.0.1:not-a-port",
"http://127.0.0.1:1234/path",
"http://[invalid",
],
)
def test_secret_broker_rejects_non_loopback_or_invalid_endpoints(endpoint):
with pytest.raises(WorkspaceSecretBrokerError):
WorkspaceSecretBroker(
endpoint=endpoint,
capability="x" * 43,
)
def test_secret_broker_rejects_mismatched_verification_identity():
endpoint, _, thread = _serve_once(
{
"statuses": [
{
**_identity().__dict__,
"space_id": "different-space",
"state": "available",
}
]
}
)
broker = WorkspaceSecretBroker(endpoint=endpoint, capability="x" * 43)
with pytest.raises(WorkspaceSecretBrokerError) as caught:
broker.verify(_identity())
thread.join(timeout=2)
assert "invalid verification" in str(caught.value)
def test_secret_broker_batch_preserves_partial_states_without_values():
identities = tuple(
WorkspaceSecretIdentity(
**{
**_identity().__dict__,
"secret_ref": f"wsvault_{index:032d}",
"slot_id": f"environment:SLOT_{index}",
}
)
for index in range(3)
)
endpoint, requests, thread = _serve_once(
{
"statuses": [
{**identities[0].__dict__, "state": "available"},
{**identities[1].__dict__, "state": "missing"},
{**identities[2].__dict__, "state": "needs_rebind"},
]
}
)
broker = WorkspaceSecretBroker(endpoint=endpoint, capability="x" * 43)
verifications = broker.verify_many(identities)
thread.join(timeout=2)
assert [item.state for item in verifications] == [
"available",
"missing",
"needs_rebind",
]
assert len(requests) == 1
assert requests[0]["path"] == "/v1/workspace-secrets/verify-batch"
assert "value" not in repr(verifications)
def test_secret_broker_rejects_value_bearing_batch_response():
sentinel = "must-never-cross-broker"
identity = _identity()
endpoint, _, thread = _serve_once(
{
"statuses": [
{
**identity.__dict__,
"state": "available",
"value": sentinel,
}
]
}
)
broker = WorkspaceSecretBroker(endpoint=endpoint, capability="x" * 43)
with pytest.raises(WorkspaceSecretBrokerError) as caught:
broker.verify_many((identity,))
thread.join(timeout=2)
assert sentinel not in str(caught.value)
def test_secret_broker_rejects_more_than_100_without_network_io():
broker = WorkspaceSecretBroker(
endpoint="http://127.0.0.1:1",
capability="x" * 43,
)
with pytest.raises(WorkspaceSecretBrokerError, match="batch is too large"):
broker.verify_many(tuple(_identity() for _ in range(101)))
def test_secret_broker_authority_is_removed_from_child_environment():
environment = {
"PATH": "/usr/bin",
"EIGENT_WORKSPACE_SECRET_BROKER_ENDPOINT": "http://127.0.0.1:1234",
"EIGENT_WORKSPACE_SECRET_BROKER_CAPABILITY": "x" * 43,
"EIGENT_WORKFORCE_SECRET_BROKER_ENDPOINT": "legacy-endpoint",
"EIGENT_WORKFORCE_SECRET_BROKER_CAPABILITY": "legacy-capability",
}
captured = _capture_broker_environment(environment)
assert captured == ("http://127.0.0.1:1234", "x" * 43)
assert environment == {"PATH": "/usr/bin"}

View file

@ -3,8 +3,14 @@ from __future__ import annotations
import hashlib
import json
import pytest
from app.run_journal import SQLiteRunJournal
from app.workspace_config import ThinkingEffort, WorkforceBundleManifest
from app.workspace_config import (
ThinkingEffort,
WorkforceBundleManifest,
WorkspaceBundleReconfigurationPendingError,
)
from app.workspace_config.admission import (
EnvironmentAdmissionService,
LegacyEnvironmentImporter,
@ -383,7 +389,7 @@ def test_materialized_bundle_replaces_legacy_template_for_new_run(tmp_path):
expected_version=proposal.version,
state="materializing",
)
journal.transition_workspace_bundle_install_proposal(
proposal = journal.transition_workspace_bundle_install_proposal(
proposal.proposal_id,
expected_version=proposal.version,
state="materialized",
@ -425,3 +431,90 @@ def test_materialized_bundle_replaces_legacy_template_for_new_run(tmp_path):
event_json = json.dumps(event.payload)
assert "private-connection-id" not in event_json
assert str(docs) not in event_json
journal.put_workspace_bundle_install_proposal(
proposal_id="proposal-review-only",
request_id="install-review-only",
space_id="space-1",
bundle_id="bundle-team",
revision_id="bundle-team@1",
config_placement="sidecar",
manifest=manifest,
assets=[],
install_plan=proposal.install_plan,
)
journal.ensure_run(
run_id="run-while-reviewing",
project_id="project-1",
status="pending",
)
while_reviewing = EnvironmentAdmissionService(journal).persist_for_run(
run_id="run-while-reviewing",
space_id="space-1",
working_directory=tmp_path,
created_by="user-1",
template=legacy,
)
assert while_reviewing.binding.bundle_revision_id == "bundle-team@1"
assert while_reviewing.spec.local_materialization.connector_bindings[
0
].local_binding_id == "private-connection-id"
_, pending = journal.put_workspace_bundle_local_binding(
proposal_id=proposal.proposal_id,
expected_proposal_version=proposal.version,
slot_id="github_readonly",
binding_kind="connector",
connector_id="github",
opaque_connection_id="replacement-connection-id",
local_path=None,
required_grants=["repository.read"],
authorized_by="user-1",
)
assert pending.state == "needs_attention"
assert pending.error_code == "bundle_reconfiguration_pending"
journal.ensure_run(
run_id="run-after-rebind",
project_id="project-1",
status="pending",
)
with pytest.raises(
WorkspaceBundleReconfigurationPendingError,
match="must be synced",
) as error:
EnvironmentAdmissionService(journal).persist_for_run(
run_id="run-after-rebind",
space_id="space-1",
working_directory=tmp_path,
created_by="user-1",
template=legacy,
)
assert error.value.code == "workspace_bundle_reconfiguration_pending"
assert error.value.proposal_id == proposal.proposal_id
assert journal.list_events("run-after-rebind") == []
materializing = journal.transition_workspace_bundle_install_proposal(
pending.proposal_id,
expected_version=pending.version,
state="materializing",
)
journal.ensure_run(
run_id="run-during-resync",
project_id="project-1",
status="pending",
)
with pytest.raises(
WorkspaceBundleReconfigurationPendingError,
match="must be synced",
) as resync_error:
EnvironmentAdmissionService(journal).persist_for_run(
run_id="run-during-resync",
space_id="space-1",
working_directory=tmp_path,
created_by="user-1",
template=legacy,
)
assert resync_error.value.state == materializing.state
assert journal.list_events("run-during-resync") == []

View file

@ -83,6 +83,12 @@ import {
isBinaryExists,
} from './utils/process';
import { WebViewManager } from './webview';
import {
closeWorkspaceSecretBroker,
ensureWorkspaceSecretBroker,
getDefaultWorkspaceSecretVault,
registerWorkspaceSecretIpcHandlers,
} from './workspaceSecrets';
const userData = app.getPath('userData');
@ -860,6 +866,11 @@ const checkManagerInstance = (manager: any, name: string) => {
function registerIpcHandlers() {
registerCodexSubscriptionAuthIpcHandlers(ipcMain);
registerTerminalIpcHandlers();
registerWorkspaceSecretIpcHandlers(
ipcMain,
getDefaultWorkspaceSecretVault(),
assertMainRendererSender
);
// ==================== auth callback ====================
ipcMain.handle('get-auth-callback-url', async () => {
@ -3377,6 +3388,7 @@ const checkAndStartBackend = async (
if (isToolInstalled.success) {
log.info('Tool installed, starting backend service...');
const codexResolverEnv = await getCodexResolverEnv();
const workspaceSecretBroker = await ensureWorkspaceSecretBroker();
const exampleSkillsDir = getExampleSkillsSourceDir();
// Start backend and wait for health check to pass
@ -3390,6 +3402,10 @@ const checkAndStartBackend = async (
EIGENT_EXAMPLE_SKILLS_DIR: exampleSkillsDir,
EIGENT_LOCAL_CONTROL_CAPABILITY: localControlCapability,
EIGENT_DESKTOP_INSTANCE_ID: resolveDesktopInstanceId(),
EIGENT_WORKSPACE_SECRET_BROKER_ENDPOINT:
workspaceSecretBroker.endpoint,
EIGENT_WORKSPACE_SECRET_BROKER_CAPABILITY:
workspaceSecretBroker.capability,
}
);
@ -3735,6 +3751,7 @@ app.on('before-quit', async (event) => {
// Wait for Python process cleanup
await cleanupPythonProcess();
await closeWorkspaceSecretBroker();
// Clean up file reader if exists
if (fileReader) {

View file

@ -76,7 +76,7 @@ function defaultShell(): { file: string; args: string[] } {
* proxy configuration, and toolchain settings.
*/
const SENSITIVE_ENV_NAME =
/(?:^|_)(?:API_KEY|TOKEN|PASSWORD|PASSWD|SECRET|PRIVATE_KEY)(?:$|_)/i;
/(?:^|_)(?:API_KEY|TOKEN|PASSWORD|PASSWD|SECRET|PRIVATE_KEY|CAPABILITY)(?:$|_)/i;
export function terminalEnvironment(
environment: NodeJS.ProcessEnv = process.env

View file

@ -0,0 +1,247 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import crypto from 'node:crypto';
import http, { type IncomingMessage, type ServerResponse } from 'node:http';
import type {
WorkspaceSecretBrokerRuntime,
WorkspaceSecretLookup,
} from './types';
import {
WorkspaceSecretBindingMismatchError,
WorkspaceSecretVault,
WorkspaceSecretVaultError,
} from './vault';
const LOOPBACK_HOST = '127.0.0.1';
const MAX_REQUEST_BODY_BYTES = 16 * 1024;
const MAX_BATCH_BINDINGS = 100;
const REQUEST_TIMEOUT_MS = 5_000;
function sendJson(
response: ServerResponse,
status: number,
body: Record<string, unknown>
): void {
const value = JSON.stringify(body);
response.writeHead(status, {
'cache-control': 'no-store',
'content-type': 'application/json; charset=utf-8',
'content-length': Buffer.byteLength(value),
pragma: 'no-cache',
});
response.end(value);
}
function isAuthorized(request: IncomingMessage, capability: string): boolean {
const value = request.headers.authorization;
if (typeof value !== 'string') return false;
const actual = Buffer.from(value, 'utf8');
const expected = Buffer.from(`Bearer ${capability}`, 'utf8');
return (
actual.length === expected.length &&
crypto.timingSafeEqual(actual, expected)
);
}
function parseLookup(value: unknown): WorkspaceSecretLookup {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new WorkspaceSecretVaultError('Invalid workspace secret request');
}
const body = value as Record<string, unknown>;
const values = [
body.secret_ref,
body.account_scope_digest,
body.space_id,
body.revision_id,
body.slot_id,
];
if (values.some((item) => typeof item !== 'string')) {
throw new WorkspaceSecretVaultError('Invalid workspace secret request');
}
return {
secret_ref: body.secret_ref as string,
account_scope_digest: body.account_scope_digest as string,
space_id: body.space_id as string,
revision_id: body.revision_id as string,
slot_id: body.slot_id as string,
};
}
function parseLookupBatch(value: unknown): WorkspaceSecretLookup[] {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new WorkspaceSecretVaultError('Invalid workspace secret request');
}
const bindings = (value as Record<string, unknown>).bindings;
if (
!Array.isArray(bindings) ||
bindings.length === 0 ||
bindings.length > MAX_BATCH_BINDINGS
) {
throw new WorkspaceSecretVaultError('Invalid workspace secret batch');
}
return bindings.map(parseLookup);
}
async function readJsonBody(request: IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let size = 0;
let settled = false;
const fail = (error: Error) => {
if (settled) return;
settled = true;
reject(error);
};
request.setTimeout(REQUEST_TIMEOUT_MS, () => {
fail(new WorkspaceSecretVaultError('Workspace secret request timed out'));
request.destroy();
});
request.on('data', (chunk: Buffer | string) => {
if (settled) return;
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
size += buffer.length;
if (size > MAX_REQUEST_BODY_BYTES) {
fail(
new WorkspaceSecretVaultError('Workspace secret request too large')
);
chunks.length = 0;
request.removeAllListeners('data');
request.resume();
return;
}
chunks.push(buffer);
});
request.on('error', fail);
request.on('end', () => {
if (settled) return;
settled = true;
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
} catch {
reject(new WorkspaceSecretVaultError('Invalid JSON request'));
}
});
});
}
/**
* A process-local verification channel for the Brain.
*
* Deliberately does not expose decryption or secret values. Runtime secret
* injection requires a stronger process-bound design and must not be added to
* this loopback bearer service.
*/
export class WorkspaceSecretBroker {
private server: http.Server | null = null;
private runtime: WorkspaceSecretBrokerRuntime | null = null;
constructor(
private readonly vault: WorkspaceSecretVault,
private readonly randomBytes: (size: number) => Buffer = crypto.randomBytes
) {}
async start(): Promise<WorkspaceSecretBrokerRuntime> {
if (this.runtime) return this.runtime;
const capability = this.randomBytes(32).toString('base64url');
const server = http.createServer((request, response) => {
void this.handle(request, response, capability);
});
server.requestTimeout = REQUEST_TIMEOUT_MS;
server.headersTimeout = REQUEST_TIMEOUT_MS;
server.keepAliveTimeout = 1_000;
server.maxRequestsPerSocket = 100;
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, LOOPBACK_HOST, () => {
server.off('error', reject);
resolve();
});
});
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
throw new Error('Workspace secret broker did not bind a TCP port');
}
this.server = server;
this.runtime = {
endpoint: `http://${LOOPBACK_HOST}:${address.port}`,
capability,
close: async () => this.close(),
};
return this.runtime;
}
async close(): Promise<void> {
const server = this.server;
this.server = null;
this.runtime = null;
if (!server) return;
await new Promise<void>((resolve) => {
server.close(() => resolve());
server.closeAllConnections?.();
});
}
private async handle(
request: IncomingMessage,
response: ServerResponse,
capability: string
): Promise<void> {
response.setHeader('connection', 'close');
if (!isAuthorized(request, capability)) {
sendJson(response, 401, { error_code: 'unauthorized' });
return;
}
if (request.method !== 'POST') {
sendJson(response, 405, { error_code: 'method_not_allowed' });
return;
}
const isSingleVerify = request.url === '/v1/workspace-secrets/verify';
const isBatchVerify = request.url === '/v1/workspace-secrets/verify-batch';
if (!isSingleVerify && !isBatchVerify) {
sendJson(response, 404, { error_code: 'not_found' });
return;
}
try {
const body = await readJsonBody(request);
if (isBatchVerify) {
const statuses = parseLookupBatch(body).map((lookup) =>
this.vault.status(lookup)
);
sendJson(response, 200, { statuses });
} else {
const status = this.vault.status(parseLookup(body));
const code =
status.state === 'available'
? 200
: status.state === 'needs_rebind'
? 409
: 404;
sendJson(response, code, { status });
}
} catch (error) {
if (error instanceof WorkspaceSecretBindingMismatchError) {
sendJson(response, 403, {
error_code: 'binding_scope_mismatch',
});
} else if (error instanceof WorkspaceSecretVaultError) {
const status = error.message.includes('too large') ? 413 : 400;
sendJson(response, status, { error_code: 'invalid_request' });
} else {
sendJson(response, 500, { error_code: 'secret_broker_failed' });
}
}
}
}

View file

@ -0,0 +1,24 @@
export { WorkspaceSecretBroker } from './broker';
export { registerWorkspaceSecretIpcHandlers } from './ipc';
export {
closeWorkspaceSecretBroker,
ensureWorkspaceSecretBroker,
getDefaultWorkspaceSecretVault,
} from './runtime';
export type {
WorkspaceSecretBrokerRuntime,
WorkspaceSecretLookup,
WorkspaceSecretPutRequest,
WorkspaceSecretPutResult,
WorkspaceSecretScope,
WorkspaceSecretState,
WorkspaceSecretStatus,
} from './types';
export {
MAX_WORKSPACE_SECRET_BYTES,
WorkspaceSecretBindingMismatchError,
WorkspaceSecretNeedsRebindError,
WorkspaceSecretNotFoundError,
WorkspaceSecretVault,
WorkspaceSecretVaultError,
} from './vault';

View file

@ -0,0 +1,47 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
import type { WorkspaceSecretLookup, WorkspaceSecretPutRequest } from './types';
import type { WorkspaceSecretVault } from './vault';
export type WorkspaceSecretIpcGuard = (event: IpcMainInvokeEvent) => void;
export function registerWorkspaceSecretIpcHandlers(
ipcMain: IpcMain,
vault: WorkspaceSecretVault,
guard: WorkspaceSecretIpcGuard
): void {
ipcMain.handle(
'workspace-secret:put',
(event, request: WorkspaceSecretPutRequest) => {
guard(event);
return vault.put(request);
}
);
ipcMain.handle(
'workspace-secret:status',
(event, request: WorkspaceSecretLookup) => {
guard(event);
return vault.status(request);
}
);
ipcMain.handle(
'workspace-secret:delete',
(event, request: WorkspaceSecretLookup) => {
guard(event);
return vault.delete(request);
}
);
}

View file

@ -0,0 +1,45 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { WorkspaceSecretBroker } from './broker';
import type { WorkspaceSecretBrokerRuntime } from './types';
import { WorkspaceSecretVault } from './vault';
let defaultVault: WorkspaceSecretVault | null = null;
let defaultBroker: WorkspaceSecretBroker | null = null;
let brokerStart: Promise<WorkspaceSecretBrokerRuntime> | null = null;
export function getDefaultWorkspaceSecretVault(): WorkspaceSecretVault {
defaultVault ??= new WorkspaceSecretVault();
return defaultVault;
}
export function ensureWorkspaceSecretBroker(): Promise<WorkspaceSecretBrokerRuntime> {
if (!brokerStart) {
defaultBroker = new WorkspaceSecretBroker(getDefaultWorkspaceSecretVault());
brokerStart = defaultBroker.start().catch((error) => {
defaultBroker = null;
brokerStart = null;
throw error;
});
}
return brokerStart;
}
export async function closeWorkspaceSecretBroker(): Promise<void> {
const broker = defaultBroker;
defaultBroker = null;
brokerStart = null;
await broker?.close();
}

View file

@ -0,0 +1,46 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
export interface WorkspaceSecretScope {
account_scope_digest: string;
space_id: string;
revision_id: string;
slot_id: string;
}
export interface WorkspaceSecretLookup extends WorkspaceSecretScope {
secret_ref: string;
}
export interface WorkspaceSecretPutRequest extends WorkspaceSecretScope {
value: string;
}
export type WorkspaceSecretState = 'available' | 'missing' | 'needs_rebind';
export interface WorkspaceSecretStatus extends WorkspaceSecretLookup {
state: WorkspaceSecretState;
created_at?: string;
updated_at?: string;
}
export interface WorkspaceSecretPutResult extends WorkspaceSecretStatus {
state: 'available';
}
export interface WorkspaceSecretBrokerRuntime {
endpoint: string;
capability: string;
close: () => Promise<void>;
}

View file

@ -0,0 +1,426 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { safeStorage } from 'electron';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type {
WorkspaceSecretLookup,
WorkspaceSecretPutRequest,
WorkspaceSecretPutResult,
WorkspaceSecretScope,
WorkspaceSecretStatus,
} from './types';
const FORMAT_VERSION = 1;
const VAULT_FILE_NAME = 'workforce-secret-vault.v1.json';
const MAX_VAULT_BYTES = 8 * 1024 * 1024;
export const MAX_WORKSPACE_SECRET_BYTES = 64 * 1024;
interface WorkspaceSecretCrypto {
isEncryptionAvailable(): boolean;
encryptString(value: string): Buffer;
decryptString(value: Buffer): string;
getSelectedStorageBackend?(): string;
}
interface EncryptedWorkspaceSecretRecord extends WorkspaceSecretScope {
secret_ref: string;
ciphertext: string;
created_at: string;
updated_at: string;
}
interface VaultDocument {
version: 1;
records: Record<string, EncryptedWorkspaceSecretRecord>;
}
interface VaultReadResult {
document: VaultDocument | null;
corrupted: boolean;
}
export interface WorkspaceSecretVaultOptions {
rootDir?: string;
crypto?: WorkspaceSecretCrypto;
platform?: NodeJS.Platform;
now?: () => Date;
randomBytes?: (size: number) => Buffer;
}
export class WorkspaceSecretVaultError extends Error {}
export class WorkspaceSecretBindingMismatchError extends WorkspaceSecretVaultError {}
export class WorkspaceSecretNeedsRebindError extends WorkspaceSecretVaultError {}
export class WorkspaceSecretNotFoundError extends WorkspaceSecretVaultError {}
function defaultCrypto(): WorkspaceSecretCrypto {
return {
isEncryptionAvailable: () => safeStorage.isEncryptionAvailable(),
encryptString: (value) => safeStorage.encryptString(value),
decryptString: (value) => safeStorage.decryptString(value),
getSelectedStorageBackend:
typeof safeStorage.getSelectedStorageBackend === 'function'
? () => safeStorage.getSelectedStorageBackend()
: undefined,
};
}
function hasOnlySafeIdentityCharacters(value: string): boolean {
return (
value.length > 0 &&
value.length <= 256 &&
!/[\u0000-\u001f\u007f]/u.test(value)
);
}
function assertScope(scope: WorkspaceSecretScope): void {
if (!/^[a-f0-9]{64}$/u.test(scope.account_scope_digest)) {
throw new WorkspaceSecretVaultError(
'Workspace secret account scope digest is invalid'
);
}
for (const [name, value] of Object.entries({
space_id: scope.space_id,
revision_id: scope.revision_id,
slot_id: scope.slot_id,
})) {
if (!hasOnlySafeIdentityCharacters(value)) {
throw new WorkspaceSecretVaultError(
`Workspace secret ${name} is invalid`
);
}
}
}
function sameScope(
record: WorkspaceSecretScope,
expected: WorkspaceSecretScope
): boolean {
return (
record.account_scope_digest === expected.account_scope_digest &&
record.space_id === expected.space_id &&
record.revision_id === expected.revision_id &&
record.slot_id === expected.slot_id
);
}
function isEncryptedRecord(
value: unknown
): value is EncryptedWorkspaceSecretRecord {
if (!value || typeof value !== 'object') return false;
const record = value as Partial<EncryptedWorkspaceSecretRecord>;
return (
typeof record.secret_ref === 'string' &&
typeof record.account_scope_digest === 'string' &&
typeof record.space_id === 'string' &&
typeof record.revision_id === 'string' &&
typeof record.slot_id === 'string' &&
typeof record.ciphertext === 'string' &&
typeof record.created_at === 'string' &&
typeof record.updated_at === 'string'
);
}
export class WorkspaceSecretVault {
readonly rootDir: string;
readonly filePath: string;
private readonly encryption: WorkspaceSecretCrypto;
private readonly platform: NodeJS.Platform;
private readonly now: () => Date;
private readonly randomBytes: (size: number) => Buffer;
constructor(options: WorkspaceSecretVaultOptions = {}) {
this.rootDir =
options.rootDir ?? path.join(os.homedir(), '.eigent', 'secure');
this.filePath = path.join(this.rootDir, VAULT_FILE_NAME);
this.encryption = options.crypto ?? defaultCrypto();
this.platform = options.platform ?? process.platform;
this.now = options.now ?? (() => new Date());
this.randomBytes = options.randomBytes ?? crypto.randomBytes;
}
put(request: WorkspaceSecretPutRequest): WorkspaceSecretPutResult {
assertScope(request);
this.assertEncryptionAvailable();
const valueBytes = Buffer.byteLength(request.value, 'utf8');
if (valueBytes === 0 || valueBytes > MAX_WORKSPACE_SECRET_BYTES) {
throw new WorkspaceSecretVaultError(
'Workspace secret value size is invalid'
);
}
const read = this.readDocument();
if (read.corrupted) {
throw new WorkspaceSecretNeedsRebindError(
'Workspace secret vault is corrupted and must be recovered'
);
}
const document = read.document ?? { version: FORMAT_VERSION, records: {} };
// References are immutable commit candidates. Reusing one would let a
// second renderer overwrite ciphertext before the Brain binding CAS wins.
const secretRef = this.newSecretRef();
const timestamp = this.now().toISOString();
const record: EncryptedWorkspaceSecretRecord = {
secret_ref: secretRef,
account_scope_digest: request.account_scope_digest,
space_id: request.space_id,
revision_id: request.revision_id,
slot_id: request.slot_id,
ciphertext: this.encryption
.encryptString(request.value)
.toString('base64'),
created_at: timestamp,
updated_at: timestamp,
};
document.records[secretRef] = record;
this.atomicWrite(document);
return this.statusFromRecord(record, 'available');
}
status(lookup: WorkspaceSecretLookup): WorkspaceSecretStatus {
assertScope(lookup);
this.assertSecretRef(lookup.secret_ref);
const read = this.readDocument();
if (read.corrupted) {
return { ...lookup, state: 'needs_rebind' };
}
const record = read.document?.records[lookup.secret_ref];
if (!record) return { ...lookup, state: 'missing' };
this.assertMatchingScope(record, lookup);
if (!this.canDecrypt(record)) {
return this.statusFromRecord(record, 'needs_rebind');
}
return this.statusFromRecord(record, 'available');
}
resolve(lookup: WorkspaceSecretLookup): string {
assertScope(lookup);
this.assertSecretRef(lookup.secret_ref);
const read = this.readDocument();
if (read.corrupted) {
throw new WorkspaceSecretNeedsRebindError(
'Workspace secret vault must be rebound'
);
}
const record = read.document?.records[lookup.secret_ref];
if (!record) {
throw new WorkspaceSecretNotFoundError('Workspace secret is missing');
}
this.assertMatchingScope(record, lookup);
this.assertEncryptionAvailable();
try {
return this.encryption.decryptString(
Buffer.from(record.ciphertext, 'base64')
);
} catch {
throw new WorkspaceSecretNeedsRebindError(
'Workspace secret must be rebound'
);
}
}
delete(lookup: WorkspaceSecretLookup): WorkspaceSecretStatus {
assertScope(lookup);
this.assertSecretRef(lookup.secret_ref);
const read = this.readDocument();
if (read.corrupted) {
throw new WorkspaceSecretNeedsRebindError(
'Workspace secret vault must be recovered before deleting records'
);
}
const document = read.document;
const record = document?.records[lookup.secret_ref];
if (!document || !record) return { ...lookup, state: 'missing' };
this.assertMatchingScope(record, lookup);
delete document.records[lookup.secret_ref];
this.atomicWrite(document);
return { ...lookup, state: 'missing' };
}
private assertEncryptionAvailable(): void {
if (!this.encryption.isEncryptionAvailable()) {
throw new WorkspaceSecretVaultError(
'OS-level encryption is unavailable; refusing plaintext storage'
);
}
if (
this.platform === 'linux' &&
this.encryption.getSelectedStorageBackend?.() === 'basic_text'
) {
throw new WorkspaceSecretVaultError(
'Linux safeStorage selected the insecure basic_text backend'
);
}
}
private canDecrypt(record: EncryptedWorkspaceSecretRecord): boolean {
try {
this.assertEncryptionAvailable();
this.encryption.decryptString(Buffer.from(record.ciphertext, 'base64'));
return true;
} catch {
return false;
}
}
private assertMatchingScope(
record: EncryptedWorkspaceSecretRecord,
lookup: WorkspaceSecretLookup
): void {
if (!sameScope(record, lookup)) {
throw new WorkspaceSecretBindingMismatchError(
'Workspace secret reference does not match its binding scope'
);
}
}
private assertSecretRef(value: string): void {
if (!/^wsvault_[A-Za-z0-9_-]{32}$/u.test(value)) {
throw new WorkspaceSecretVaultError(
'Workspace secret reference is invalid'
);
}
}
private newSecretRef(): string {
return `wsvault_${this.randomBytes(24).toString('base64url')}`;
}
private statusFromRecord(
record: EncryptedWorkspaceSecretRecord,
state: 'available'
): WorkspaceSecretPutResult;
private statusFromRecord(
record: EncryptedWorkspaceSecretRecord,
state: 'needs_rebind'
): WorkspaceSecretStatus;
private statusFromRecord(
record: EncryptedWorkspaceSecretRecord,
state: 'available' | 'needs_rebind'
): WorkspaceSecretStatus {
return {
secret_ref: record.secret_ref,
account_scope_digest: record.account_scope_digest,
space_id: record.space_id,
revision_id: record.revision_id,
slot_id: record.slot_id,
state,
created_at: record.created_at,
updated_at: record.updated_at,
};
}
private readDocument(): VaultReadResult {
if (!fs.existsSync(this.filePath)) {
return { document: null, corrupted: false };
}
try {
const stat = fs.lstatSync(this.filePath);
if (
stat.isSymbolicLink() ||
!stat.isFile() ||
stat.size > MAX_VAULT_BYTES
) {
return { document: null, corrupted: true };
}
const parsed = JSON.parse(
fs.readFileSync(this.filePath, 'utf8')
) as Partial<VaultDocument>;
if (
parsed.version !== FORMAT_VERSION ||
!parsed.records ||
typeof parsed.records !== 'object'
) {
return { document: null, corrupted: true };
}
for (const [secretRef, record] of Object.entries(parsed.records)) {
if (
!isEncryptedRecord(record) ||
record.secret_ref !== secretRef ||
!/^wsvault_[A-Za-z0-9_-]{32}$/u.test(secretRef)
) {
return { document: null, corrupted: true };
}
assertScope(record);
}
return { document: parsed as VaultDocument, corrupted: false };
} catch {
return { document: null, corrupted: true };
}
}
private ensureSecureDirectory(): void {
if (fs.existsSync(this.rootDir)) {
const stat = fs.lstatSync(this.rootDir);
if (stat.isSymbolicLink() || !stat.isDirectory()) {
throw new WorkspaceSecretVaultError(
'Workspace secret vault directory is unsafe'
);
}
} else {
fs.mkdirSync(this.rootDir, { recursive: true, mode: 0o700 });
}
try {
fs.chmodSync(this.rootDir, 0o700);
} catch {
// Windows and some filesystems do not implement POSIX permissions.
}
}
private atomicWrite(document: VaultDocument): void {
this.ensureSecureDirectory();
const serialized = JSON.stringify(document);
if (Buffer.byteLength(serialized, 'utf8') > MAX_VAULT_BYTES) {
throw new WorkspaceSecretVaultError('Workspace secret vault is full');
}
const temporaryPath = path.join(
this.rootDir,
`.${VAULT_FILE_NAME}.${process.pid}.${this.randomBytes(8).toString('hex')}.tmp`
);
let descriptor: number | null = null;
try {
descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
fs.writeFileSync(descriptor, serialized, 'utf8');
fs.fsyncSync(descriptor);
fs.closeSync(descriptor);
descriptor = null;
fs.renameSync(temporaryPath, this.filePath);
try {
fs.chmodSync(this.filePath, 0o600);
} catch {
// Best effort on filesystems without POSIX permissions.
}
try {
const directoryDescriptor = fs.openSync(this.rootDir, 'r');
try {
fs.fsyncSync(directoryDescriptor);
} finally {
fs.closeSync(directoryDescriptor);
}
} catch {
// Directory fsync is unavailable on Windows and some filesystems.
}
} finally {
if (descriptor !== null) fs.closeSync(descriptor);
try {
fs.unlinkSync(temporaryPath);
} catch {
// The successful rename already removed the temporary path.
}
}
}
}

View file

@ -13,6 +13,10 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { contextBridge, ipcRenderer, webUtils } from 'electron';
import type {
WorkspaceSecretLookup,
WorkspaceSecretPutRequest,
} from '../main/workspaceSecrets/types';
contextBridge.exposeInMainWorld('ipcRenderer', {
on(...args: Parameters<typeof ipcRenderer.on>) {
@ -103,6 +107,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
envRemove: (email: string, key: string) =>
ipcRenderer.invoke('env-remove', email, key),
getEnvPath: (email: string) => ipcRenderer.invoke('get-env-path', email),
workspaceSecretPut: (request: WorkspaceSecretPutRequest) =>
ipcRenderer.invoke('workspace-secret:put', request),
workspaceSecretStatus: (request: WorkspaceSecretLookup) =>
ipcRenderer.invoke('workspace-secret:status', request),
workspaceSecretDelete: (request: WorkspaceSecretLookup) =>
ipcRenderer.invoke('workspace-secret:delete', request),
// command execution
executeCommand: (command: string, email: string) =>
ipcRenderer.invoke('execute-command', command, email),

View file

@ -0,0 +1,470 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
fetchReview: vi.fn(),
fetchProposal: vi.fn(),
createProposal: vi.fn(),
decide: vi.fn(),
bindValues: vi.fn(),
bindPath: vi.fn(),
bindConnector: vi.fn(),
approveScript: vi.fn(),
materialize: vi.fn(),
digest: vi.fn(),
fetchConnected: vi.fn(),
secretPut: vi.fn(),
secretDelete: vi.fn(),
selectFile: vi.fn(),
createSpace: vi.fn(),
deleteSpace: vi.fn(),
ensureScratch: vi.fn(),
setActiveSpace: vi.fn(),
setActiveProject: vi.fn(),
setActiveWorkspaceTab: vi.fn(),
}));
vi.mock('@/service/workspaceBundleInstallApi', () => ({
parseWorkspaceBundleHandle: (value: string) => {
const match = /^([A-Za-z0-9][A-Za-z0-9._-]{0,79})@([1-9][0-9]*)$/.exec(
value
);
return match ? { bundleId: match[1], revisionId: value } : null;
},
fetchWorkspaceBundleInstallReview: mocks.fetchReview,
fetchWorkspaceBundleInstallProposal: mocks.fetchProposal,
createWorkspaceBundleInstallProposal: mocks.createProposal,
decideWorkspaceBundleInstall: mocks.decide,
bindWorkspaceBundleLocalValues: mocks.bindValues,
bindWorkspaceBundleLocalPath: mocks.bindPath,
bindWorkspaceBundleConnector: mocks.bindConnector,
approveWorkspaceBundleScript: mocks.approveScript,
materializeWorkspaceBundle: mocks.materialize,
workspaceBundleAccountScopeDigest: mocks.digest,
}));
vi.mock('@/api/connectors', () => ({
fetchConnectedProviders: mocks.fetchConnected,
providerLabel: (provider: { service: string }) => provider.service,
}));
vi.mock('@/host', () => ({
useHost: () => ({
electronAPI: {
workspaceSecretPut: mocks.secretPut,
workspaceSecretDelete: mocks.secretDelete,
selectFile: mocks.selectFile,
},
}),
}));
vi.mock('@/lib/scratchSpaceWorkspace', () => ({
ensureScratchSpaceWorkspaceBinding: mocks.ensureScratch,
}));
vi.mock('@/store/authStore', () => ({
useAuthStore: (selector: (state: object) => unknown) =>
selector({ email: 'owner@example.com', user_id: 'user-1' }),
}));
vi.mock('@/store/spaceStore', () => {
const state = {
createSpaceOnServer: mocks.createSpace,
deleteSpaceOnServer: mocks.deleteSpace,
setActiveSpace: mocks.setActiveSpace,
getSpaceById: (spaceId: string) => ({
id: spaceId,
name: 'Imported',
sourceType: 'blank',
}),
};
const useSpaceStore = Object.assign(
(selector: (value: typeof state) => unknown) => selector(state),
{ getState: () => state }
);
return { useSpaceStore };
});
vi.mock('@/store/projectRuntimeStore', () => ({
useProjectRuntimeStore: () => ({ setActiveProject: mocks.setActiveProject }),
}));
vi.mock('@/store/pageTabStore', () => ({
usePageTabStore: (selector: (state: object) => unknown) =>
selector({ setActiveWorkspaceTab: mocks.setActiveWorkspaceTab }),
}));
import type { WorkspaceBundleInstallSnapshot } from '@/service/workspaceBundleInstallApi';
import { WorkspaceBundleInstallWizard } from './WorkspaceBundleInstallWizard';
const manifest = {
apiVersion: 'eigent.ai/v1alpha1',
kind: 'WorkforceBundle',
metadata: { id: 'research', name: 'Research workforce', revision: 1 },
spec: {
instructions: {},
context: [],
skills: [],
connectors: [],
mcpServers: [],
environment: {
variables: [
{
name: 'API_TOKEN',
required: true,
sensitive: true,
description: 'Research API token',
},
],
},
agents: [],
models: {
default: { modelRef: 'provider://default', thinkingEffort: 'medium' },
},
permissions: { profile: 'request_approval', rules: [] },
git: {
enabled: true,
checkpointPolicy: 'user_and_run_terminal',
agentIsolation: 'worktree',
remotePolicy: 'prompt',
},
},
} as const;
const review = {
bundle: {
id: 'research',
workspace_id: 'author-space',
name: 'Research workforce',
visibility: 'public' as const,
latest_published_revision_id: 'research@1',
},
revision: {
id: 'research@1',
bundle_id: 'research',
revision: 1,
manifest,
manifest_digest: 'a'.repeat(64),
status: 'published' as const,
assets: [],
},
};
const snapshot = (configured = false): WorkspaceBundleInstallSnapshot => ({
proposal: {
proposal_id: 'proposal-1',
request_id: 'request-1',
space_id: 'space-1',
bundle_id: 'research',
revision_id: 'research@1',
config_placement: 'sidecar',
state: 'approved',
version: configured ? 3 : 2,
manifest:
manifest as WorkspaceBundleInstallSnapshot['proposal']['manifest'],
manifest_digest: 'a'.repeat(64),
assets: [],
install_plan: {
connector_slots: [],
local_path_slots: [],
script_actions: [],
environment_requirements: [],
mcp_secret_requirements: [],
permission_profile: 'request_approval',
git_policy: {},
asset_count: 0,
asset_bytes: 0,
},
},
bindings: [],
value_requirements: [
{
requirement_key: 'environment:API_TOKEN',
requirement_kind: 'environment',
name: 'API_TOKEN',
required: true,
sensitive: true,
configured,
available: configured,
binding_version: configured ? 1 : null,
},
],
readiness: {
ready: configured,
missing_requirements: configured ? [] : ['environment:API_TOKEN'],
},
});
function renderWizard(props: {
initialHandle?: string;
initialProposalId?: string;
}) {
return render(
<MemoryRouter>
<WorkspaceBundleInstallWizard {...props} />
</MemoryRouter>
);
}
describe('WorkspaceBundleInstallWizard', () => {
beforeEach(() => {
window.localStorage.clear();
Object.values(mocks).forEach((mock) => mock.mockReset());
mocks.fetchConnected.mockResolvedValue([]);
mocks.digest.mockResolvedValue('b'.repeat(64));
mocks.createSpace.mockResolvedValue('space-1');
mocks.ensureScratch.mockResolvedValue('/tmp/space-1');
mocks.createProposal.mockResolvedValue({
...snapshot(false),
proposal: { ...snapshot(false).proposal, state: 'proposed', version: 1 },
});
mocks.decide.mockResolvedValue(snapshot(false));
});
it('shows the immutable review before creating or approving a Space', async () => {
mocks.fetchReview.mockResolvedValue(review);
const user = userEvent.setup();
renderWizard({ initialHandle: 'research@1' });
expect(await screen.findByText('Research workforce')).toBeInTheDocument();
expect(mocks.createSpace).not.toHaveBeenCalled();
expect(mocks.decide).not.toHaveBeenCalled();
await user.click(
screen.getByRole('button', { name: /confirm and create/i })
);
await waitFor(() => expect(mocks.decide).toHaveBeenCalledTimes(1));
expect(mocks.createSpace).toHaveBeenCalledTimes(1);
expect(mocks.createProposal.mock.invocationCallOrder[0]).toBeLessThan(
mocks.decide.mock.invocationCallOrder[0]
);
});
it('stores plaintext only through the Electron vault and binds an opaque ref', async () => {
mocks.fetchProposal.mockResolvedValue(snapshot(false));
mocks.secretPut.mockResolvedValue({
secret_ref: `wsvault_${'A'.repeat(32)}`,
account_scope_digest: 'b'.repeat(64),
space_id: 'space-1',
revision_id: 'research@1',
slot_id: 'environment:API_TOKEN',
state: 'available',
});
mocks.bindValues.mockResolvedValue(snapshot(true));
const user = userEvent.setup();
renderWizard({ initialProposalId: 'proposal-1' });
const input = await screen.findByLabelText('Local value for API_TOKEN');
await user.type(input, 'plaintext-do-not-send-to-brain');
await user.click(screen.getByRole('button', { name: 'Save' }));
await waitFor(() => expect(mocks.bindValues).toHaveBeenCalledTimes(1));
expect(mocks.secretPut).toHaveBeenCalledWith(
expect.objectContaining({ value: 'plaintext-do-not-send-to-brain' })
);
expect(JSON.stringify(mocks.bindValues.mock.calls[0][0])).not.toContain(
'plaintext-do-not-send-to-brain'
);
expect(mocks.bindValues).toHaveBeenCalledWith(
expect.objectContaining({
bindings: [
expect.objectContaining({ secret_ref: `wsvault_${'A'.repeat(32)}` }),
],
})
);
});
it('repairs an unavailable local value with the safe binding version', async () => {
const unavailable = snapshot(false);
unavailable.value_requirements[0] = {
...unavailable.value_requirements[0],
configured: true,
available: false,
binding_version: 4,
};
mocks.fetchProposal.mockResolvedValue(unavailable);
mocks.secretPut.mockResolvedValue({
secret_ref: `wsvault_${'B'.repeat(32)}`,
account_scope_digest: 'b'.repeat(64),
space_id: 'space-1',
revision_id: 'research@1',
slot_id: 'environment:API_TOKEN',
state: 'available',
});
mocks.bindValues.mockResolvedValue({
...snapshot(true),
cleanup_secret_refs: [`wsvault_${'A'.repeat(32)}`],
});
const user = userEvent.setup();
renderWizard({ initialProposalId: 'proposal-1' });
expect(
await screen.findByText(/previous local value is unavailable/i)
).toBeInTheDocument();
await user.type(
screen.getByLabelText('Local value for API_TOKEN'),
'replacement-value'
);
await user.click(screen.getByRole('button', { name: 'Save' }));
await waitFor(() =>
expect(mocks.bindValues).toHaveBeenCalledWith(
expect.objectContaining({
bindings: [expect.objectContaining({ expected_binding_version: 4 })],
})
)
);
expect(mocks.secretDelete).toHaveBeenCalledWith(
expect.objectContaining({ secret_ref: `wsvault_${'A'.repeat(32)}` })
);
});
it('keeps local setup editable after the Workspace is installed', async () => {
const installed = snapshot(true);
installed.proposal = {
...installed.proposal,
state: 'materialized',
version: 8,
};
mocks.fetchProposal.mockResolvedValue(installed);
mocks.secretPut.mockResolvedValue({
secret_ref: `wsvault_${'C'.repeat(32)}`,
account_scope_digest: 'b'.repeat(64),
space_id: 'space-1',
revision_id: 'research@1',
slot_id: 'environment:API_TOKEN',
state: 'available',
});
mocks.bindValues.mockResolvedValue({
...installed,
proposal: { ...installed.proposal, version: 9 },
});
const user = userEvent.setup();
renderWizard({ initialProposalId: 'proposal-1' });
expect(
await screen.findByText('Workspace files installed')
).toBeInTheDocument();
await waitFor(() => expect(mocks.fetchConnected).toHaveBeenCalledTimes(1));
await user.type(
screen.getByLabelText('Local value for API_TOKEN'),
'rotated-value'
);
await user.click(screen.getByRole('button', { name: 'Replace' }));
await waitFor(() =>
expect(mocks.bindValues).toHaveBeenCalledWith(
expect.objectContaining({
expectedVersion: 8,
bindings: [expect.objectContaining({ expected_binding_version: 1 })],
})
)
);
});
it('reconciles the durable binding after an ambiguous response loss', async () => {
mocks.fetchProposal
.mockResolvedValueOnce(snapshot(false))
.mockResolvedValueOnce(snapshot(true));
mocks.secretPut.mockResolvedValue({
secret_ref: `wsvault_${'D'.repeat(32)}`,
account_scope_digest: 'b'.repeat(64),
space_id: 'space-1',
revision_id: 'research@1',
slot_id: 'environment:API_TOKEN',
state: 'available',
});
mocks.bindValues.mockRejectedValue(
new Error('Response lost after the durable commit')
);
const user = userEvent.setup();
renderWizard({ initialProposalId: 'proposal-1' });
await user.type(
await screen.findByLabelText('Local value for API_TOKEN'),
'possibly-committed-value'
);
await user.click(screen.getByRole('button', { name: 'Save' }));
expect(
await screen.findByText('Response lost after the durable commit')
).toBeInTheDocument();
await waitFor(() => expect(mocks.fetchProposal).toHaveBeenCalledTimes(2));
expect(screen.getByRole('button', { name: 'Replace' })).toBeInTheDocument();
});
it('offers a retry when the read-only review request fails', async () => {
mocks.fetchReview
.mockRejectedValueOnce(new Error('Cloud is temporarily unavailable'))
.mockResolvedValueOnce(review);
const user = userEvent.setup();
renderWizard({ initialHandle: 'research@1' });
expect(
await screen.findByText('Cloud is temporarily unavailable')
).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Retry' }));
expect(await screen.findByText('Research workforce')).toBeInTheDocument();
expect(mocks.fetchReview).toHaveBeenCalledTimes(2);
});
it('retries a proposal failure without creating a second Space', async () => {
mocks.fetchReview.mockResolvedValue(review);
mocks.createProposal
.mockRejectedValueOnce(new Error('Brain response was interrupted'))
.mockResolvedValueOnce({
...snapshot(false),
proposal: {
...snapshot(false).proposal,
state: 'proposed',
version: 1,
},
});
const user = userEvent.setup();
renderWizard({ initialHandle: 'research@1' });
await user.click(
await screen.findByRole('button', { name: /confirm and create/i })
);
expect(
await screen.findByText('Brain response was interrupted')
).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Retry' }));
await waitFor(() => expect(mocks.decide).toHaveBeenCalledTimes(1));
expect(mocks.createProposal).toHaveBeenCalledTimes(2);
expect(mocks.createSpace).toHaveBeenCalledTimes(1);
});
it('recovers the inactive Space seed after a renderer restart', async () => {
mocks.fetchReview.mockResolvedValue(review);
mocks.createProposal.mockRejectedValueOnce(
new Error('Brain stopped before the proposal commit')
);
const user = userEvent.setup();
const first = renderWizard({ initialHandle: 'research@1' });
await user.click(
await screen.findByRole('button', { name: /confirm and create/i })
);
expect(
await screen.findByText('Brain stopped before the proposal commit')
).toBeInTheDocument();
first.unmount();
mocks.createProposal.mockResolvedValueOnce({
...snapshot(false),
proposal: { ...snapshot(false).proposal, state: 'proposed', version: 1 },
});
renderWizard({ initialHandle: 'research@1' });
await user.click(
await screen.findByRole('button', { name: /confirm and create/i })
);
await waitFor(() => expect(mocks.decide).toHaveBeenCalledTimes(1));
expect(mocks.createSpace).toHaveBeenCalledTimes(1);
expect(mocks.createProposal).toHaveBeenCalledTimes(2);
});
});

File diff suppressed because it is too large Load diff

View file

@ -41,6 +41,7 @@ import {
FolderOpen,
LayoutGrid,
List,
PackagePlus,
PlusCircle,
} from 'lucide-react';
import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
@ -243,6 +244,18 @@ export default function HomeHubToolbar({
</span>
</TooltipSimple>
<Button
type="button"
variant="secondary"
size="sm"
buttonContent="text"
buttonRadius="full"
onClick={() => navigate('/workspace-bundles/install')}
>
<PackagePlus className="h-4 w-4 shrink-0" aria-hidden />
Import Workforce Bundle
</Button>
<DropdownMenu>
<TooltipSimple content={sortLabel} variant="instant">
<span className="inline-flex">

View file

@ -0,0 +1,14 @@
import { WorkspaceBundleInstallWizard } from '@/components/WorkspaceBundle/WorkspaceBundleInstallWizard';
import { useSearchParams } from 'react-router-dom';
export default function WorkspaceBundleInstall() {
const [searchParams] = useSearchParams();
return (
<main className="h-full overflow-y-auto bg-ds-bg-neutral-muted-default px-6 py-8">
<WorkspaceBundleInstallWizard
initialHandle={searchParams.get('handle') || ''}
initialProposalId={searchParams.get('proposal') || ''}
/>
</main>
);
}

View file

@ -12,6 +12,10 @@ import { Textarea } from '@/components/ui/textarea';
import { EnvironmentRequirementsEditor } from '@/components/WorkspaceConfiguration/EnvironmentRequirementsEditor';
import { WorkspaceBundleSaveDialog } from '@/components/WorkspaceConfiguration/WorkspaceBundleSaveDialog';
import { useWorkspaceConfiguration } from '@/hooks/useWorkspaceConfiguration';
import {
fetchWorkspaceBundleInstallForSpace,
type WorkspaceBundleInstallProposal,
} from '@/service/workspaceBundleInstallApi';
import {
workspaceEnvironmentVariables,
type ThinkingEffort,
@ -27,11 +31,13 @@ import {
KeyRound,
Plus,
RefreshCw,
Settings2,
Share2,
ShieldCheck,
Trash2,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
const csv = (value: string): string[] =>
value
@ -116,7 +122,10 @@ const selectClassName =
'h-10 w-full rounded-xl border border-solid border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default px-3 text-body-sm text-ds-text-neutral-default-default outline-none focus:ring-1 focus:ring-ds-ring-brand-default-focus';
export default function WorkspaceConfiguration() {
const navigate = useNavigate();
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const [installedBundle, setInstalledBundle] =
useState<WorkspaceBundleInstallProposal | null>(null);
const activeSpaceId = useSpaceStore((state) => state.activeSpaceId);
const activeSpace = useSpaceStore((state) =>
state.activeSpaceId ? state.spaces[state.activeSpaceId] : null
@ -134,6 +143,23 @@ export default function WorkspaceConfiguration() {
identity,
});
useEffect(() => {
let active = true;
setInstalledBundle(null);
if (!activeSpaceId) return () => undefined;
void fetchWorkspaceBundleInstallForSpace(activeSpaceId)
.then((snapshot) => {
if (active) setInstalledBundle(snapshot.proposal);
})
.catch(() => {
// A 404 means this is a locally authored Workspace. The configuration
// editor remains fully usable without an installation proposal.
});
return () => {
active = false;
};
}, [activeSpaceId]);
const update = useCallback(
(mutate: (current: WorkspaceConfigurationDocument) => void) => {
setDocument((current) => {
@ -214,6 +240,21 @@ export default function WorkspaceConfiguration() {
Retry
</Button>
) : null}
{installedBundle ? (
<Button
type="button"
variant="secondary"
size="sm"
onClick={() =>
navigate(
`/workspace-bundles/install?proposal=${encodeURIComponent(installedBundle.proposal_id)}`
)
}
>
<Settings2 className="h-4 w-4" aria-hidden />
Local setup
</Button>
) : null}
<Button
type="button"
size="sm"

View file

@ -29,6 +29,9 @@ const RemoteControl = lazy(() => import('@/pages/RemoteControl'));
const WorkspaceConfiguration = lazy(
() => import('@/pages/WorkspaceConfiguration')
);
const WorkspaceBundleInstall = lazy(
() => import('@/pages/WorkspaceBundleInstall')
);
const IS_LOCAL_MODE = import.meta.env.VITE_USE_LOCAL_PROXY === 'true';
const ENABLE_DESKTOP_REMOTE_CONTROL_FALLBACK = isDesktop();
@ -171,6 +174,10 @@ const AppRoutes = () => (
path="/workspace-configuration"
element={<WorkspaceConfiguration />}
/>
<Route
path="/workspace-bundles/install"
element={<WorkspaceBundleInstall />}
/>
<Route
path="/setting"
element={<Navigate to="/history?tab=settings" replace />}

View file

@ -0,0 +1,145 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
fetchGet: vi.fn(),
fetchPost: vi.fn(),
fetchPut: vi.fn(),
findBundle: vi.fn(),
getRevision: vi.fn(),
}));
vi.mock('@/api/http', () => ({
fetchGet: mocks.fetchGet,
fetchPost: mocks.fetchPost,
fetchPut: mocks.fetchPut,
}));
vi.mock('./workspaceBundleAuthoringApi', () => ({
findWorkspaceBundle: mocks.findBundle,
getWorkspaceBundleRevision: mocks.getRevision,
}));
import {
bindWorkspaceBundleLocalValues,
createWorkspaceBundleInstallProposal,
fetchWorkspaceBundleInstallForSpace,
fetchWorkspaceBundleInstallReview,
parseWorkspaceBundleHandle,
} from './workspaceBundleInstallApi';
describe('workspace Bundle install API', () => {
beforeEach(() => {
Object.values(mocks).forEach((mock) => mock.mockReset());
});
it('accepts only a canonical immutable share handle', () => {
expect(parseWorkspaceBundleHandle('research-workforce@12')).toEqual({
bundleId: 'research-workforce',
revisionId: 'research-workforce@12',
});
expect(parseWorkspaceBundleHandle('research-workforce')).toBeNull();
expect(parseWorkspaceBundleHandle('research-workforce@0')).toBeNull();
});
it('loads the published revision before creating a local proposal', async () => {
mocks.findBundle.mockResolvedValue({ id: 'research-workforce' });
mocks.getRevision.mockResolvedValue({
id: 'research-workforce@1',
bundle_id: 'research-workforce',
status: 'published',
});
await fetchWorkspaceBundleInstallReview({
bundleId: 'research-workforce',
revisionId: 'research-workforce@1',
});
expect(mocks.getRevision).toHaveBeenCalledWith(
'research-workforce',
'research-workforce@1'
);
expect(mocks.fetchPost).not.toHaveBeenCalled();
});
it('rejects a draft revision during the review-first read', async () => {
mocks.findBundle.mockResolvedValue({ id: 'research-workforce' });
mocks.getRevision.mockResolvedValue({
id: 'research-workforce@1',
bundle_id: 'research-workforce',
status: 'validated',
});
await expect(
fetchWorkspaceBundleInstallReview({
bundleId: 'research-workforce',
revisionId: 'research-workforce@1',
})
).rejects.toThrow('Only published');
});
it('creates the durable proposal with sidecar placement', async () => {
mocks.fetchPost.mockResolvedValue({ proposal: { proposal_id: 'p-1' } });
await createWorkspaceBundleInstallProposal({
proposalId: 'p-1',
requestId: 'r-1',
spaceId: 'space-1',
bundleId: 'research-workforce',
revisionId: 'research-workforce@1',
});
expect(mocks.fetchPost).toHaveBeenCalledWith(
'/api/v1/workspace-bundles/install-proposals',
expect.objectContaining({
proposal_id: 'p-1',
config_placement: 'sidecar',
})
);
});
it('loads the durable installation attached to a Space', async () => {
mocks.fetchGet.mockResolvedValue({
proposal: { proposal_id: 'proposal-1' },
});
await fetchWorkspaceBundleInstallForSpace('space / one');
expect(mocks.fetchGet).toHaveBeenCalledWith(
'/api/v1/spaces/space%20%2F%20one/workspace-bundle-installation'
);
});
it('sends only opaque vault references to Brain, never plaintext', async () => {
mocks.fetchPut.mockResolvedValue({ proposal: { proposal_id: 'p-1' } });
const plaintext = 'secret-value-that-must-not-cross-ipc';
await bindWorkspaceBundleLocalValues({
proposalId: 'p-1',
clientRequestId: 'bind-1',
expectedVersion: 3,
actorId: 'user-1',
bindings: [
{
requirement_key: 'environment:API_TOKEN',
requirement_kind: 'environment',
secret_ref: 'wsvault_opaque-reference',
account_scope_digest: 'a'.repeat(64),
expected_binding_version: null,
},
],
});
const serializedPayload = JSON.stringify(mocks.fetchPut.mock.calls[0][1]);
expect(serializedPayload).not.toContain(plaintext);
expect(serializedPayload).toContain('wsvault_opaque-reference');
expect(mocks.fetchPut.mock.calls[0][1].bindings[0]).not.toHaveProperty(
'value'
);
expect(mocks.fetchPut).toHaveBeenCalledWith(
'/api/v1/workspace-bundles/install-proposals/p-1/local-values',
expect.objectContaining({
bindings: [expect.objectContaining({ expected_binding_version: null })],
})
);
});
});

View file

@ -0,0 +1,286 @@
import { fetchGet, fetchPost, fetchPut } from '@/api/http';
import {
findWorkspaceBundle,
getWorkspaceBundleRevision,
type CloudWorkspaceBundle,
type CloudWorkspaceBundleRevision,
} from './workspaceBundleAuthoringApi';
import type { WorkspaceConfigurationDocument } from './workspaceConfigurationApi';
export type WorkspaceBundleInstallState =
| 'proposed'
| 'approved'
| 'rejected'
| 'materializing'
| 'materialized'
| 'needs_attention';
export interface WorkspaceBundleValueRequirement {
requirement_key: string;
requirement_kind: 'environment' | 'mcp_secret';
configured: boolean;
available: boolean;
binding_version: number | null;
required: boolean;
name?: string;
mcp_id?: string;
slot_id?: string;
sensitive?: boolean;
description?: string | null;
example?: string | null;
}
export interface WorkspaceBundleInstallPlan {
connector_slots: Array<{
slot_id: string;
connector_id: string;
required_grants: string[];
}>;
local_path_slots: string[];
script_actions: string[];
environment_requirements: Array<Record<string, unknown>>;
mcp_secret_requirements: Array<Record<string, unknown>>;
permission_profile: string;
git_policy: Record<string, unknown>;
asset_count: number;
asset_bytes: number;
}
export interface WorkspaceBundleInstallProposal {
proposal_id: string;
request_id: string;
space_id: string;
bundle_id: string;
revision_id: string;
config_placement: 'in_repo' | 'sidecar';
state: WorkspaceBundleInstallState;
version: number;
manifest: WorkspaceConfigurationDocument;
manifest_digest: string;
assets: Array<{
id: string;
logical_path: string;
content_digest: string;
media_type: string;
size_bytes: number;
}>;
install_plan: WorkspaceBundleInstallPlan;
error_code?: string | null;
}
export interface WorkspaceBundleInstallBinding {
slot_id: string;
binding_kind: 'connector' | 'local_path' | 'script_approval';
connector_id?: string | null;
opaque_connection_id?: string | null;
local_path?: string | null;
required_grants: string[];
}
export interface WorkspaceBundleInstallSnapshot {
proposal: WorkspaceBundleInstallProposal;
bindings: WorkspaceBundleInstallBinding[];
value_requirements: WorkspaceBundleValueRequirement[];
readiness: {
ready: boolean;
missing_requirements: string[];
};
/** Exact prior refs replaced by this CAS; present only on local-value PUT. */
cleanup_secret_refs?: string[];
}
export interface WorkspaceBundleInstallReview {
bundle: CloudWorkspaceBundle | null;
revision: CloudWorkspaceBundleRevision;
}
export interface ParsedWorkspaceBundleHandle {
bundleId: string;
revisionId: string;
}
export function parseWorkspaceBundleHandle(
input: string
): ParsedWorkspaceBundleHandle | null {
const value = input.trim();
const match = /^([A-Za-z0-9][A-Za-z0-9._-]{0,79})@([1-9][0-9]*)$/.exec(value);
if (!match) return null;
return { bundleId: match[1], revisionId: value };
}
export async function fetchWorkspaceBundleInstallReview(
handle: ParsedWorkspaceBundleHandle
): Promise<WorkspaceBundleInstallReview> {
const revision = await getWorkspaceBundleRevision(
handle.bundleId,
handle.revisionId
);
if (revision.status !== 'published') {
throw new Error(
'Only published Workforce Bundle versions can be installed.'
);
}
if (
revision.bundle_id !== handle.bundleId ||
revision.id !== handle.revisionId
) {
throw new Error('The Workforce Bundle version identity does not match.');
}
// Public install remains usable even if mutable owner metadata is not
// readable by this account. The immutable revision is the authority.
const bundle = await findWorkspaceBundle(handle.bundleId).catch(() => null);
return { bundle, revision };
}
export const createWorkspaceBundleInstallProposal = async (input: {
proposalId: string;
requestId: string;
spaceId: string;
bundleId: string;
revisionId: string;
}): Promise<WorkspaceBundleInstallSnapshot> =>
fetchPost('/api/v1/workspace-bundles/install-proposals', {
proposal_id: input.proposalId,
request_id: input.requestId,
space_id: input.spaceId,
bundle_id: input.bundleId,
revision_id: input.revisionId,
config_placement: 'sidecar',
});
export const fetchWorkspaceBundleInstallProposal = async (
proposalId: string
): Promise<WorkspaceBundleInstallSnapshot> =>
fetchGet(
`/api/v1/workspace-bundles/install-proposals/${encodeURIComponent(proposalId)}`
);
export const fetchWorkspaceBundleInstallForSpace = async (
spaceId: string
): Promise<WorkspaceBundleInstallSnapshot> =>
fetchGet(
`/api/v1/spaces/${encodeURIComponent(spaceId)}/workspace-bundle-installation`
);
export const decideWorkspaceBundleInstall = async (input: {
proposalId: string;
expectedVersion: number;
approved: boolean;
actorId: string;
}): Promise<WorkspaceBundleInstallSnapshot> =>
fetchPost(
`/api/v1/workspace-bundles/install-proposals/${encodeURIComponent(input.proposalId)}/decision`,
{
expected_version: input.expectedVersion,
approved: input.approved,
actor_id: input.actorId,
}
);
export const bindWorkspaceBundleConnector = async (input: {
proposalId: string;
expectedVersion: number;
slotId: string;
connectorId: string;
connectionId: string;
actorId: string;
}): Promise<WorkspaceBundleInstallSnapshot> =>
fetchPost(
`/api/v1/workspace-bundles/install-proposals/${encodeURIComponent(input.proposalId)}/connector-bindings`,
{
expected_version: input.expectedVersion,
slot_id: input.slotId,
connector_id: input.connectorId,
connection_id: input.connectionId,
actor_id: input.actorId,
}
);
export const bindWorkspaceBundleLocalPath = async (input: {
proposalId: string;
expectedVersion: number;
slotId: string;
localPath: string;
actorId: string;
}): Promise<WorkspaceBundleInstallSnapshot> =>
fetchPost(
`/api/v1/workspace-bundles/install-proposals/${encodeURIComponent(input.proposalId)}/local-path-bindings`,
{
expected_version: input.expectedVersion,
slot_id: input.slotId,
local_path: input.localPath,
actor_id: input.actorId,
}
);
export const approveWorkspaceBundleScript = async (input: {
proposalId: string;
expectedVersion: number;
actionId: string;
actorId: string;
}): Promise<WorkspaceBundleInstallSnapshot> =>
fetchPost(
`/api/v1/workspace-bundles/install-proposals/${encodeURIComponent(input.proposalId)}/script-approvals`,
{
expected_version: input.expectedVersion,
action_id: input.actionId,
actor_id: input.actorId,
}
);
export interface WorkspaceBundleOpaqueValueBinding {
requirement_key: string;
requirement_kind: 'environment' | 'mcp_secret';
secret_ref: string;
account_scope_digest: string;
expected_binding_version: number | null;
}
export const bindWorkspaceBundleLocalValues = async (input: {
proposalId: string;
clientRequestId: string;
expectedVersion: number;
actorId: string;
bindings: WorkspaceBundleOpaqueValueBinding[];
}): Promise<WorkspaceBundleInstallSnapshot> =>
fetchPut(
`/api/v1/workspace-bundles/install-proposals/${encodeURIComponent(input.proposalId)}/local-values`,
{
client_request_id: input.clientRequestId,
expected_version: input.expectedVersion,
actor_id: input.actorId,
bindings: input.bindings,
}
);
export const materializeWorkspaceBundle = async (input: {
proposalId: string;
expectedVersion: number;
email: string;
userId?: string | number | null;
actorId: string;
}): Promise<WorkspaceBundleInstallSnapshot> =>
fetchPost(
`/api/v1/workspace-bundles/install-proposals/${encodeURIComponent(input.proposalId)}/materialize`,
{
expected_version: input.expectedVersion,
email: input.email,
...(input.userId === undefined || input.userId === null
? {}
: { user_id: input.userId }),
actor_id: input.actorId,
allow_content_repository_init: false,
}
);
export async function workspaceBundleAccountScopeDigest(
actorId: string
): Promise<string> {
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(`eigent-account:${actorId.trim().toLowerCase()}`)
);
return Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, '0')
).join('');
}

View file

@ -116,6 +116,52 @@ interface ElectronAPI {
envWrite: (email: string, kv: { key: string; value: string }) => Promise<any>;
envRemove: (email: string, key: string) => Promise<any>;
getEnvPath: (email: string) => Promise<string>;
workspaceSecretPut: (request: {
account_scope_digest: string;
space_id: string;
revision_id: string;
slot_id: string;
value: string;
}) => Promise<{
secret_ref: string;
account_scope_digest: string;
space_id: string;
revision_id: string;
slot_id: string;
state: 'available';
created_at?: string;
updated_at?: string;
}>;
workspaceSecretStatus: (request: {
secret_ref: string;
account_scope_digest: string;
space_id: string;
revision_id: string;
slot_id: string;
}) => Promise<{
secret_ref: string;
account_scope_digest: string;
space_id: string;
revision_id: string;
slot_id: string;
state: 'available' | 'missing' | 'needs_rebind';
created_at?: string;
updated_at?: string;
}>;
workspaceSecretDelete: (request: {
secret_ref: string;
account_scope_digest: string;
space_id: string;
revision_id: string;
slot_id: string;
}) => Promise<{
secret_ref: string;
account_scope_digest: string;
space_id: string;
revision_id: string;
slot_id: string;
state: 'missing';
}>;
executeCommand: (
command: string,
email: string

View file

@ -143,6 +143,8 @@ describe('terminal IPC lifecycle', () => {
GH_TOKEN: 'secret',
AWS_SECRET_ACCESS_KEY: 'secret',
CODEX_RESOLVER_SECRET: 'secret',
EIGENT_WORKSPACE_SECRET_BROKER_CAPABILITY: 'secret',
EIGENT_WORKFORCE_SECRET_BROKER_CAPABILITY: 'legacy-secret',
})
).toEqual({
PATH: '/usr/bin',

View file

@ -0,0 +1,27 @@
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
describe('Workspace secret Electron wiring', () => {
it('guards IPC with the main renderer and supplies only broker coordinates to Brain', () => {
const source = fs.readFileSync(
path.resolve(process.cwd(), 'electron/main/index.ts'),
'utf8'
);
expect(source).toMatch(
/registerWorkspaceSecretIpcHandlers\([\s\S]*?assertMainRendererSender[\s\S]*?\)/u
);
expect(source).toContain('EIGENT_WORKSPACE_SECRET_BROKER_ENDPOINT');
expect(source).toContain('EIGENT_WORKSPACE_SECRET_BROKER_CAPABILITY');
expect(source).not.toMatch(
/EIGENT_WORKSPACE_SECRET_(?:VALUE|TOKEN|PLAINTEXT)/u
);
const terminalSource = fs.readFileSync(
path.resolve(process.cwd(), 'electron/main/terminal.ts'),
'utf8'
);
expect(terminalSource).toMatch(/PRIVATE_KEY\|CAPABILITY/u);
});
});

View file

@ -0,0 +1,450 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('electron', () => ({
safeStorage: {
isEncryptionAvailable: () => true,
getSelectedStorageBackend: () => 'kwallet6',
encryptString: (value: string) => Buffer.from(value),
decryptString: (value: Buffer) => value.toString('utf8'),
},
}));
import { WorkspaceSecretBroker } from '../../../../electron/main/workspaceSecrets/broker';
import { registerWorkspaceSecretIpcHandlers } from '../../../../electron/main/workspaceSecrets/ipc';
import type {
WorkspaceSecretLookup,
WorkspaceSecretPutRequest,
} from '../../../../electron/main/workspaceSecrets/types';
import {
WorkspaceSecretBindingMismatchError,
WorkspaceSecretVault,
} from '../../../../electron/main/workspaceSecrets/vault';
function fakeCrypto(
options: {
available?: boolean;
backend?: string;
failDecrypt?: boolean;
} = {}
) {
return {
isEncryptionAvailable: () => options.available ?? true,
getSelectedStorageBackend: () => options.backend ?? 'kwallet6',
encryptString: (value: string) =>
Buffer.from(`cipher:${Buffer.from(value).toString('base64')}`, 'utf8'),
decryptString: (value: Buffer) => {
if (options.failDecrypt) throw new Error('rotated keychain');
const encoded = value.toString('utf8').replace(/^cipher:/u, '');
return Buffer.from(encoded, 'base64').toString('utf8');
},
};
}
const scope = {
account_scope_digest: 'a'.repeat(64),
space_id: 'space-1',
revision_id: 'bundle@1',
slot_id: 'mcp.github.env.GITHUB_TOKEN',
};
describe('WorkspaceSecretVault', () => {
let rootDir: string;
beforeEach(() => {
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-vault-test-'));
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(rootDir, { recursive: true, force: true });
});
function vault(
options: Parameters<typeof fakeCrypto>[0] = {},
platform: NodeJS.Platform = 'darwin'
) {
return new WorkspaceSecretVault({
rootDir: path.join(rootDir, 'secure'),
crypto: fakeCrypto(options),
platform,
now: () => new Date('2026-08-11T00:00:00.000Z'),
});
}
it('encrypts each value and writes only scoped metadata with secure permissions', () => {
const store = vault();
const fsync = vi.spyOn(fs, 'fsyncSync');
const result = store.put({ ...scope, value: 'sentinel-super-secret' });
const disk = fs.readFileSync(store.filePath, 'utf8');
expect(result.state).toBe('available');
expect(result.secret_ref).toMatch(/^wsvault_[A-Za-z0-9_-]{32}$/u);
expect(result).not.toHaveProperty('value');
expect(disk).not.toContain('sentinel-super-secret');
expect(disk).not.toContain(
Buffer.from('sentinel-super-secret').toString('base64')
);
expect(disk).toContain(scope.slot_id);
expect(fs.statSync(store.rootDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(store.filePath).mode & 0o777).toBe(0o600);
expect(fs.readdirSync(store.rootDir)).toEqual([
'workforce-secret-vault.v1.json',
]);
expect(fsync).toHaveBeenCalledTimes(2);
expect(store.resolve(result)).toBe('sentinel-super-secret');
});
it('creates an immutable candidate reference when the tuple is rebound', () => {
const store = vault();
const first = store.put({ ...scope, value: 'first-value' });
const second = store.put({ ...scope, value: 'second-value' });
expect(second.secret_ref).not.toBe(first.secret_ref);
expect(store.resolve(first)).toBe('first-value');
expect(store.resolve(second)).toBe('second-value');
});
it('rejects Linux basic_text rather than creating a plaintext fallback', () => {
const store = vault({ backend: 'basic_text' }, 'linux');
expect(() => store.put({ ...scope, value: 'must-not-be-written' })).toThrow(
/basic_text/
);
expect(fs.existsSync(store.filePath)).toBe(false);
});
it('fails closed when OS encryption is unavailable', () => {
const store = vault({ available: false });
expect(() =>
store.put({ ...scope, value: 'must-remain-memory-only' })
).toThrow(/encryption is unavailable/);
expect(fs.existsSync(store.filePath)).toBe(false);
});
it('reports unreadable ciphertext as needs_rebind without returning a value', () => {
const writer = vault();
const created = writer.put({ ...scope, value: 'rotating-secret' });
const reader = vault({ failDecrypt: true });
const result = reader.status(created);
expect(result.state).toBe('needs_rebind');
expect(result).not.toHaveProperty('value');
expect(() => reader.resolve(created)).toThrow(/rebound/);
});
it('rejects a secret_ref replayed under a different binding tuple', () => {
const store = vault();
const created = store.put({ ...scope, value: 'scoped-secret' });
expect(() => store.status({ ...created, space_id: 'other-space' })).toThrow(
WorkspaceSecretBindingMismatchError
);
});
it('preserves the previous durable record if the atomic rename fails', () => {
const store = vault();
const created = store.put({ ...scope, value: 'durable-old-value' });
vi.spyOn(fs, 'renameSync').mockImplementationOnce(() => {
throw new Error('simulated crash before rename');
});
expect(() =>
store.put({ ...scope, value: 'uncommitted-new-value' })
).toThrow(/simulated crash/);
expect(store.resolve(created)).toBe('durable-old-value');
expect(
fs.readdirSync(store.rootDir).filter((name) => name.endsWith('.tmp'))
).toEqual([]);
});
it('turns a malformed vault into needs_rebind and refuses destructive overwrite', () => {
const store = vault();
fs.mkdirSync(store.rootDir, { recursive: true });
fs.writeFileSync(store.filePath, '{truncated');
const lookup: WorkspaceSecretLookup = {
...scope,
secret_ref: `wsvault_${'A'.repeat(32)}`,
};
expect(store.status(lookup).state).toBe('needs_rebind');
expect(() => store.put({ ...scope, value: 'new-value' })).toThrow(
/corrupted/
);
expect(fs.readFileSync(store.filePath, 'utf8')).toBe('{truncated');
});
});
describe('workspace secret IPC', () => {
it('runs the main-renderer guard before every vault operation', () => {
const handlers = new Map<string, (...args: any[]) => unknown>();
const ipcMain = {
handle: vi.fn((channel: string, handler: (...args: any[]) => unknown) => {
handlers.set(channel, handler);
}),
};
const vault = {
put: vi.fn(),
status: vi.fn(),
delete: vi.fn(),
};
const guard = vi.fn(() => {
throw new Error('untrusted sender');
});
registerWorkspaceSecretIpcHandlers(ipcMain as any, vault as any, guard);
for (const channel of [
'workspace-secret:put',
'workspace-secret:status',
'workspace-secret:delete',
]) {
expect(() => handlers.get(channel)!({ sender: { id: 99 } }, {})).toThrow(
/untrusted sender/
);
}
expect(guard).toHaveBeenCalledTimes(3);
expect(vault.put).not.toHaveBeenCalled();
expect(vault.status).not.toHaveBeenCalled();
expect(vault.delete).not.toHaveBeenCalled();
});
});
describe('WorkspaceSecretBroker', () => {
let rootDir: string;
let broker: WorkspaceSecretBroker | null = null;
beforeEach(() => {
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-broker-test-'));
});
afterEach(async () => {
await broker?.close();
broker = null;
fs.rmSync(rootDir, { recursive: true, force: true });
});
it('requires its random capability and only reveals binding status', async () => {
const store = new WorkspaceSecretVault({
rootDir: path.join(rootDir, 'secure'),
crypto: fakeCrypto(),
});
const request: WorkspaceSecretPutRequest = {
...scope,
value: 'broker-must-not-return-this',
};
const created = store.put(request);
broker = new WorkspaceSecretBroker(store, () => Buffer.alloc(32, 7));
const runtime = await broker.start();
expect(runtime.endpoint).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/u);
const unauthorized = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/verify`,
{
method: 'POST',
body: JSON.stringify(created),
}
);
expect(unauthorized.status).toBe(401);
const authorized = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/verify`,
{
method: 'POST',
headers: {
authorization: `Bearer ${runtime.capability}`,
'content-type': 'application/json',
},
body: JSON.stringify(created),
}
);
expect(authorized.status).toBe(200);
const body = await authorized.text();
expect(body).toContain('available');
expect(body).not.toContain('broker-must-not-return-this');
const resolveAttempt = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/resolve`,
{
method: 'POST',
headers: { authorization: `Bearer ${runtime.capability}` },
body: JSON.stringify(created),
}
);
expect(resolveAttempt.status).toBe(404);
});
it('verifies up to 100 bindings in one ordered response without values', async () => {
const lookups: WorkspaceSecretLookup[] = [
{
...scope,
secret_ref: `wsvault_${'A'.repeat(32)}`,
slot_id: 'available-slot',
},
{
...scope,
secret_ref: `wsvault_${'B'.repeat(32)}`,
slot_id: 'missing-slot',
},
{
...scope,
secret_ref: `wsvault_${'C'.repeat(32)}`,
slot_id: 'rebind-slot',
},
];
const store = {
status: vi.fn((lookup: WorkspaceSecretLookup) => ({
...lookup,
state:
lookup.slot_id === 'available-slot'
? 'available'
: lookup.slot_id === 'missing-slot'
? 'missing'
: 'needs_rebind',
})),
};
broker = new WorkspaceSecretBroker(
store as unknown as WorkspaceSecretVault
);
const runtime = await broker.start();
const response = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/verify-batch`,
{
method: 'POST',
headers: {
authorization: `Bearer ${runtime.capability}`,
'content-type': 'application/json',
},
body: JSON.stringify({ bindings: lookups }),
}
);
const body = await response.json();
expect(response.status).toBe(200);
expect(body.statuses.map((item: { state: string }) => item.state)).toEqual([
'available',
'missing',
'needs_rebind',
]);
expect(JSON.stringify(body)).not.toContain('value');
expect(store.status).toHaveBeenCalledTimes(3);
});
it('rejects batches larger than 100 before consulting the vault', async () => {
const store = { status: vi.fn() };
broker = new WorkspaceSecretBroker(
store as unknown as WorkspaceSecretVault
);
const runtime = await broker.start();
const response = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/verify-batch`,
{
method: 'POST',
headers: {
authorization: `Bearer ${runtime.capability}`,
'content-type': 'application/json',
},
body: JSON.stringify({
bindings: Array.from({ length: 101 }, () => ({
secret_ref: 'x',
account_scope_digest: 'x',
space_id: 'x',
revision_id: 'x',
slot_id: 'x',
})),
}),
}
);
expect(response.status).toBe(400);
expect(store.status).not.toHaveBeenCalled();
});
it('rejects a valid reference presented with different metadata', async () => {
const store = new WorkspaceSecretVault({
rootDir: path.join(rootDir, 'secure'),
crypto: fakeCrypto(),
});
const created = store.put({ ...scope, value: 'tuple-bound' });
broker = new WorkspaceSecretBroker(store);
const runtime = await broker.start();
const response = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/verify`,
{
method: 'POST',
headers: { authorization: `Bearer ${runtime.capability}` },
body: JSON.stringify({ ...created, revision_id: 'bundle@2' }),
}
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
error_code: 'binding_scope_mismatch',
});
});
it('fails the whole batch when any binding tuple is mismatched', async () => {
const store = new WorkspaceSecretVault({
rootDir: path.join(rootDir, 'secure'),
crypto: fakeCrypto(),
});
const created = store.put({ ...scope, value: 'tuple-bound-batch' });
broker = new WorkspaceSecretBroker(store);
const runtime = await broker.start();
const response = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/verify-batch`,
{
method: 'POST',
headers: {
authorization: `Bearer ${runtime.capability}`,
'content-type': 'application/json',
},
body: JSON.stringify({
bindings: [created, { ...created, slot_id: 'different-slot' }],
}),
}
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
error_code: 'binding_scope_mismatch',
});
});
it('rejects oversized request bodies before parsing them', async () => {
const store = new WorkspaceSecretVault({
rootDir: path.join(rootDir, 'secure'),
crypto: fakeCrypto(),
});
broker = new WorkspaceSecretBroker(store);
const runtime = await broker.start();
const response = await fetch(
`${runtime.endpoint}/v1/workspace-secrets/verify`,
{
method: 'POST',
headers: { authorization: `Bearer ${runtime.capability}` },
body: JSON.stringify({ padding: 'x'.repeat(17 * 1024) }),
}
);
expect(response.status).toBe(413);
});
});