mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-22 01:24:52 +02:00
[eric] share: user-confirmed Export anyway when the file secret heuristic trips (download only; own credential fields stay blocked)
This commit is contained in:
@@ -126,9 +126,9 @@ def build_manifest(root_type: EntityType, root_id: str) -> Manifest:
|
||||
return p_assemble(root_type, root_id)[0]
|
||||
|
||||
|
||||
def build_bundle(root_type: EntityType, root_id: str) -> tuple[bytes, str]:
|
||||
def build_bundle(root_type: EntityType, root_id: str, allow_file_secrets: bool = False) -> tuple[bytes, str]:
|
||||
manifest, payloads, files = p_assemble(root_type, root_id)
|
||||
raw = pack(manifest.model_dump(by_alias=True, mode="json"), payloads, files)
|
||||
raw = pack(manifest.model_dump(by_alias=True, mode="json"), payloads, files, allow_file_secrets=allow_file_secrets)
|
||||
return raw, manifest.root.name
|
||||
|
||||
|
||||
|
||||
@@ -104,6 +104,8 @@ class ReviewSummary(BaseModel):
|
||||
class ExportRequest(BaseModel):
|
||||
type: EntityType
|
||||
id: str
|
||||
# User-confirmed "export anyway": skips the file-content secret heuristic on direct download only; denied payload fields stay blocked.
|
||||
allow_secrets: bool = False
|
||||
|
||||
|
||||
class ExportPreflightResponse(BaseModel):
|
||||
|
||||
@@ -71,7 +71,7 @@ async def export_preflight(body: ExportRequest) -> ExportPreflightResponse:
|
||||
@swarm.router.post("/export")
|
||||
async def export_bundle(body: ExportRequest) -> Response:
|
||||
try:
|
||||
raw, name = closure.build_bundle(body.type, body.id)
|
||||
raw, name = closure.build_bundle(body.type, body.id, allow_file_secrets=body.allow_secrets)
|
||||
except BundleError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
fname = closure.swarm_filename(name)
|
||||
|
||||
@@ -37,21 +37,25 @@ def p_content_digest(entries: dict[str, bytes]) -> str:
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def pack(manifest: dict, payloads: dict[str, dict], files: dict[str, bytes]) -> bytes:
|
||||
def pack(manifest: dict, payloads: dict[str, dict], files: dict[str, bytes], allow_file_secrets: bool = False) -> bytes:
|
||||
"""payloads: bundle_id -> JSON payload (-> entities/<bid>/payload.json).
|
||||
files: full zip path -> bytes (e.g. entities/<bid>/files/<rel>)."""
|
||||
files: full zip path -> bytes (e.g. entities/<bid>/files/<rel>).
|
||||
allow_file_secrets is a user-confirmed override for the FILE-content heuristic only
|
||||
(workspace code trips it on look-alike strings); denied payload fields are our own
|
||||
credential store and are never exportable, override or not."""
|
||||
for bid, payload in payloads.items():
|
||||
leaked = find_denied_keys(payload)
|
||||
if leaked:
|
||||
raise BundleError(
|
||||
f"refusing to export: secret-shaped field(s) in {bid}: {leaked[:3]}"
|
||||
)
|
||||
leaky_files = find_secrets_in_files(files)
|
||||
if leaky_files:
|
||||
raise BundleError(
|
||||
f"refusing to export: a secret-shaped value is in {leaky_files[0]}; "
|
||||
"remove it (use an environment variable) and try again"
|
||||
)
|
||||
if not allow_file_secrets:
|
||||
leaky_files = find_secrets_in_files(files)
|
||||
if leaky_files:
|
||||
raise BundleError(
|
||||
f"refusing to export: a secret-shaped value is in {leaky_files[0]}; "
|
||||
"remove it (use an environment variable) and try again"
|
||||
)
|
||||
entries: dict[str, bytes] = {}
|
||||
for bid, payload in payloads.items():
|
||||
entries[f"entities/{bid}/payload.json"] = json.dumps(payload, indent=2).encode("utf-8")
|
||||
|
||||
@@ -110,6 +110,15 @@ def test_pack_allows_clean_workspace_file():
|
||||
assert zipfile.is_zipfile(io.BytesIO(raw))
|
||||
|
||||
|
||||
def test_pack_export_anyway_overrides_file_scan_but_never_denied_keys():
|
||||
# User-confirmed override ships a flagged workspace FILE (trusted recipient); our own credential fields stay unexportable no matter what.
|
||||
leak = b"const KEY = 'sk-ant-api03-AAAAAAAAAAAAAAAAAAAAAAAA';\n"
|
||||
raw = pack({"format_version": 1}, {"bid1": {"name": "ok"}}, {"entities/bid1/files/config.js": leak}, allow_file_secrets=True)
|
||||
assert zipfile.is_zipfile(io.BytesIO(raw))
|
||||
with pytest.raises(BundleError):
|
||||
pack({"format_version": 1}, {"bid1": {"api_key": "leak"}}, {}, allow_file_secrets=True)
|
||||
|
||||
|
||||
def test_app_export_drops_machine_env(tmp_path, monkeypatch):
|
||||
# The live .env holds the source machine's absolute paths + pinned port; it must never ride along. .env.example (portable) does.
|
||||
from backend.apps.swarm.entities import apps as appmod
|
||||
|
||||
@@ -55,11 +55,11 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
|
||||
return load();
|
||||
}, [open, load]);
|
||||
|
||||
const handleDownload = async () => {
|
||||
const handleDownload = async (allowSecrets = false) => {
|
||||
if (!preflight) return;
|
||||
setDownloading(true);
|
||||
try {
|
||||
await downloadSwarm(target, preflight.filename);
|
||||
await downloadSwarm(target, preflight.filename, allowSecrets);
|
||||
setToast(`Saved ${preflight.filename}`);
|
||||
onClose();
|
||||
} catch (e: any) {
|
||||
@@ -68,6 +68,8 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
|
||||
setDownloading(false);
|
||||
}
|
||||
};
|
||||
// The file-content secret heuristic is overridable (download goes to people you trust); our own credential fields ("secret-shaped field(s)") are not.
|
||||
const secretOverridable = error.includes('secret-shaped value');
|
||||
|
||||
const optionRow = (
|
||||
selected: boolean,
|
||||
@@ -150,6 +152,16 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
|
||||
<Button size="small" onClick={load} sx={{ textTransform: 'none', color: c.accent.primary }}>
|
||||
Try again
|
||||
</Button>
|
||||
{secretOverridable && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => { setError(''); handleDownload(true); }}
|
||||
disabled={downloading}
|
||||
sx={{ textTransform: 'none', color: c.status.error, ml: 1 }}
|
||||
>
|
||||
Export anyway (includes the flagged value; only send to people you trust)
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
) : preflight ? (
|
||||
<IncludesList summary={preflight.summary} />
|
||||
@@ -179,7 +191,7 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDownload}
|
||||
onClick={() => handleDownload()}
|
||||
disabled={!preflight || downloading}
|
||||
startIcon={
|
||||
downloading ? (
|
||||
|
||||
@@ -28,11 +28,11 @@ export async function exportPreflight(target: ShareTarget): Promise<ExportPrefli
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function downloadSwarm(target: ShareTarget, filename: string): Promise<void> {
|
||||
export async function downloadSwarm(target: ShareTarget, filename: string, allowSecrets = false): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/swarm/export`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: target.kind, id: target.id }),
|
||||
body: JSON.stringify({ type: target.kind, id: target.id, allow_secrets: allowSecrets }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await _detail(res, "We couldn't build the file."));
|
||||
const blob = await res.blob();
|
||||
|
||||
Reference in New Issue
Block a user