mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 18:35:04 -10:00
files made by a run as administrator no longer lock the user out: explicit user-SID entry and owner on create (private_mkdir, restrict_to_current_user), process default owner/DACL set to the user, self-repair of Strata's own per-user folders (APPDATA, LOCALAPPDATA, ~/.strata) on read, at host start and by 'strata repair-access', plain message when it cannot; tests; tune_session loopback allowed in the no-acquisition audit
This commit is contained in:
1 parent
e1dca4bff5
commit
df13760cf4
29 files changed
+532
-29
No files matched your search
@@ -217,6 +217,15 @@ export function createControl({ control = findControl(), env = process.env, run
|
||||
if (result.code !== 0 || !result.json?.ok) throw failure(result, 'The engine did not start.');
|
||||
return result.json;
|
||||
},
|
||||
/** Give the user back Strata files in their own folders that a run as administrator locked (`strata repair-access`). Never throws. */
|
||||
async repairAccess() {
|
||||
try {
|
||||
const result = await call(['repair-access', '--output', 'json'], { timeoutMs: 60_000 });
|
||||
return result.json ?? { ok: false, error: result.stderr };
|
||||
} catch (error) {
|
||||
return { ok: false, error: String(error?.message ?? error) };
|
||||
}
|
||||
},
|
||||
/** End servers and engines Strata started whose owner is gone (each is re-checked by PID, start time and command line). */
|
||||
async cleanup() {
|
||||
const result = await call(['runtime', 'cleanup', '--output', 'json'], { timeoutMs: 120_000 });
|
||||
|
||||
@@ -83,6 +83,8 @@ const imageModels = createImageModels({ dataDir: store.DATA_DIR, modelsDir: () =
|
||||
/* The installed app ships Strata Control (python + strata_control) next to the host; a source checkout has none and keeps its older paths. */
|
||||
const CONTROL = findControl();
|
||||
const control = createControl({ control: CONTROL });
|
||||
// Files a run as administrator made in Strata's own folders (owner Administrators) are given back to the user at every start; best effort, never blocks.
|
||||
if (control && process.platform === 'win32' && !process.env.STRATA_NO_REPAIR_ACCESS) void control.repairAccess().catch(() => {});
|
||||
const appUpdater = createAppUpdater();
|
||||
const native=createNativeControl({control,appUpdater,hardwareProbe:()=>(hardware.cachedHardware()?.gpus??[]).map((g,n)=>({index:g.index??n,vram_gb:g.vramGb})),directory:store.DATA_DIR,modelsDirectory:()=>store.MODELS_DIR,nativeDataDirectory:store.nativeDataDirectory,beforeLoad:async()=>{if(models.REAL&&models.library().loaded){if(serving.state().requests.some(r=>runtimeModels.state().instances.some(i=>i.id===r.instanceId)&&["queued","generating"].includes(r.state)))throw new Error("Finish requests before switching engines.");await models.unload();runtimeState();}},requests:()=>serving.state().requests,changed:value=>hub.send('native',value)});
|
||||
/* The agent: chats become DeepSeek Harness sessions when that component is installed. It is a separate
|
||||
|
||||
@@ -215,7 +215,12 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:300}));};
|
||||
started=startWith(cfg);
|
||||
started.then(()=>append(id,'Strata Control reports the engine is ready.'),error=>{append(id,error.message);});
|
||||
try{await started;}catch(error){const said=humanStartError(error);
|
||||
try{await started;}catch(firstError){let error=firstError;
|
||||
/* A settings, profile or run file an elevated run made can be closed to this user: repair the access in Strata's own folders and start again (this is not the memory try below). */
|
||||
const isAccess=e=>/PermissionError|access is denied|EACCES/i.test(String(e?.message??'')+JSON.stringify(e?.output??''));
|
||||
if(isAccess(error)&&typeof control.repairAccess==='function'&&job.state==='running'){await control.repairAccess();append(id,'Strata repaired access to its own files; starting again.');try{await startWith(cfg);started=Promise.resolve();error=null;}catch(again){error=again;}}
|
||||
if(error){const said=humanStartError(error);
|
||||
if(isAccess(error)){instance.status='error';instance.error=said;emit();throw new Error(said);} // access errors are not memory stops: no safer retry
|
||||
/* ONE automatic second try in all, never saved, never repeated (args.retrySafer false turns it off): with a safer memory setting when the
|
||||
failure looks like a memory stop, and without several-requests-at-once when that was the app's own default (not a number the user chose).
|
||||
Both changes ride on the same single try, with the plain sentence of the memory change when there is one. */
|
||||
@@ -228,7 +233,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
retried={words,detail};if(dropParallel){instance.parallel=1;if(!safer)parallelNote=FALLBACK_NOTE;}
|
||||
append(id,`The first start stopped (${said}). Starting once more: ${detail}`);job.summary=words;job.details=[said,`Second try: ${detail}`];instance.notice=words;emit();
|
||||
try{await startWith(retryConfig);append(id,'Strata Control reports the engine is ready (second try).');}
|
||||
catch(second){const said2=humanStartError(second);instance.status='error';instance.error=said2===said?said2:`${said2} (after one automatic second try with a safer setting: ${detail}; the first stop was: ${said})`;emit();throw new Error(instance.error);}}
|
||||
catch(second){const said2=humanStartError(second);instance.status='error';instance.error=said2===said?said2:`${said2} (after one automatic second try with a safer setting: ${detail}; the first stop was: ${said})`;emit();throw new Error(instance.error);}}}
|
||||
} else {
|
||||
proc=spawnProcess(py,[join(repo(),'serve/server.py'),'--engine','strata','--config',configPath,'--port',String(cfg.port),'--host','127.0.0.1'],{cwd:repo(),detached:process.platform!=='win32',stdio:['ignore','pipe','pipe'],env:{...process.env,PYTHONUNBUFFERED:'1',PYTHONIOENCODING:'utf-8',STRATA_API_KEY:''}});
|
||||
instances.set(id,instance);processes.set(id,proc);if(!defaultId||!args.multiple||wasDefault)defaultId=id;
|
||||
|
||||
@@ -21,4 +21,6 @@ export function makePrivate(path, { platform = process.platform } = {}) {
|
||||
// Grant first, then drop every inherited entry, so the file is never left without an owner entry.
|
||||
execFileSync(system32('icacls.exe'), [path, '/grant:r', `*${currentUserSid()}:F`, '/inheritance:r'],
|
||||
{ stdio: 'ignore', timeout: 15000, windowsHide: true });
|
||||
// The owner too, so a file an elevated run made is not left owned by Administrators (needs the right to; best effort).
|
||||
try { execFileSync(system32('icacls.exe'), [path, '/setowner', `*${currentUserSid()}`], { stdio: 'ignore', timeout: 15000, windowsHide: true }); } catch { /* the explicit entry above already opens it to the user */ }
|
||||
}
|
||||
@@ -16,6 +16,7 @@ const MEMORY_NEXT = 'Close other programs and try again, or choose “Leave 20%
|
||||
/** Order matters: the first rule that matches wins. */
|
||||
const RULES: Rule[] = [
|
||||
{ kind: 'picture-reading', match: /vision files|picture reading/i, message: 'Picture reading isn’t available for this model.', next: 'Turn off picture reading and try again.' },
|
||||
{ kind: 'access', match: /PermissionError|access is denied|can.t open its settings file/i, message: 'Strata can’t open its settings file.', next: 'Run Strata once as yourself (not as administrator) or press Repair.' },
|
||||
{ kind: 'memory', match: /server_exited|engine stopped|exited before|died before|out of memory|memory guard|\boom\b|killed/i, message: MEMORY, next: MEMORY_NEXT },
|
||||
{ kind: 'slow-load', match: /loading took more than/i, message: 'Loading is taking too long.', next: 'The PC may be short on memory. Close other programs and try again.' },
|
||||
{ kind: 'engine-missing', match: /no engine for this pc|engine component|never downloads an engine|engine .*not installed|no strata engine/i, message: 'Strata’s engine for this computer isn’t installed yet.', next: 'It is added when you download a model. Download your model again to add it.' },
|
||||
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "Einstellungen anwenden und laden",
|
||||
"s2671": "Anwenden & laden",
|
||||
"s2672": "Die Modelldateien sind bereits auf diesem Computer. Es wird nichts heruntergeladen, nur Ihre gewählten Einstellungen werden angewendet.",
|
||||
"s2673": "Aktuell geladene native Modelle werden zuerst entladen."
|
||||
"s2673": "Aktuell geladene native Modelle werden zuerst entladen.",
|
||||
"s2674": "Strata kann seine Einstellungsdatei nicht öffnen.",
|
||||
"s2675": "Starte Strata einmal als du selbst (nicht als Administrator) oder drücke auf Reparieren."
|
||||
}
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "Apply settings and load",
|
||||
"s2671": "Apply & load",
|
||||
"s2672": "The model files are already on this computer. Nothing will be downloaded; only the settings you chose are applied.",
|
||||
"s2673": "Any currently loaded native models will be unloaded first."
|
||||
"s2673": "Any currently loaded native models will be unloaded first.",
|
||||
"s2674": "Strata can’t open its settings file.",
|
||||
"s2675": "Run Strata once as yourself (not as administrator) or press Repair."
|
||||
}
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "Aplicar ajustes y cargar",
|
||||
"s2671": "Aplicar y cargar",
|
||||
"s2672": "Los archivos del modelo ya están en este ordenador. No se descargará nada; solo se aplican los ajustes que elegiste.",
|
||||
"s2673": "Los modelos nativos cargados actualmente se descargarán primero."
|
||||
"s2673": "Los modelos nativos cargados actualmente se descargarán primero.",
|
||||
"s2674": "Strata no puede abrir su archivo de ajustes.",
|
||||
"s2675": "Ejecuta Strata una vez como tú mismo (no como administrador) o pulsa Reparar."
|
||||
}
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "設定を適用して読み込む",
|
||||
"s2671": "適用して読み込む",
|
||||
"s2672": "モデルのファイルはこのコンピューターにすでにあります。ダウンロードは行われず、選んだ設定だけが適用されます。",
|
||||
"s2673": "現在読み込まれているネイティブモデルは先にアンロードされます。"
|
||||
"s2673": "現在読み込まれているネイティブモデルは先にアンロードされます。",
|
||||
"s2674": "Strata が設定ファイルを開けません。",
|
||||
"s2675": "管理者ではなくご自身のアカウントで一度 Strata を起動するか、「修復」を押してください。"
|
||||
}
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "설정 적용 후 불러오기",
|
||||
"s2671": "적용 후 불러오기",
|
||||
"s2672": "모델 파일이 이미 이 컴퓨터에 있습니다. 아무것도 다운로드하지 않으며 선택한 설정만 적용됩니다.",
|
||||
"s2673": "현재 불러온 네이티브 모델은 먼저 언로드됩니다."
|
||||
"s2673": "현재 불러온 네이티브 모델은 먼저 언로드됩니다.",
|
||||
"s2674": "Strata가 설정 파일을 열 수 없습니다.",
|
||||
"s2675": "관리자가 아닌 본인 계정으로 Strata를 한 번 실행하거나 '복구'를 누르세요."
|
||||
}
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "Aplicar configurações e carregar",
|
||||
"s2671": "Aplicar e carregar",
|
||||
"s2672": "Os arquivos do modelo já estão neste computador. Nada será baixado; apenas as configurações escolhidas serão aplicadas.",
|
||||
"s2673": "Os modelos nativos atualmente carregados serão descarregados primeiro."
|
||||
"s2673": "Os modelos nativos atualmente carregados serão descarregados primeiro.",
|
||||
"s2674": "O Strata não consegue abrir o arquivo de configurações.",
|
||||
"s2675": "Abra o Strata uma vez como você mesmo (não como administrador) ou pressione Reparar."
|
||||
}
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "Применить настройки и загрузить",
|
||||
"s2671": "Применить и загрузить",
|
||||
"s2672": "Файлы модели уже на этом компьютере. Ничего не будет скачиваться, применяются только выбранные настройки.",
|
||||
"s2673": "Сначала будут выгружены все загруженные сейчас нативные модели."
|
||||
"s2673": "Сначала будут выгружены все загруженные сейчас нативные модели.",
|
||||
"s2674": "Strata не может открыть файл настроек.",
|
||||
"s2675": "Запустите Strata один раз от своего имени (не от администратора) или нажмите «Исправить»."
|
||||
}
|
||||
@@ -2671,5 +2671,7 @@
|
||||
"s2670": "应用设置并加载",
|
||||
"s2671": "应用并加载",
|
||||
"s2672": "模型文件已在这台电脑上,不会下载任何内容,只会应用你选择的设置。",
|
||||
"s2673": "当前已加载的本地模型会先被卸载。"
|
||||
"s2673": "当前已加载的本地模型会先被卸载。",
|
||||
"s2674": "Strata 无法打开它的设置文件。",
|
||||
"s2675": "请以你自己的身份(不要用管理员身份)运行一次 Strata,或点击“修复”。"
|
||||
}
|
||||
@@ -85,7 +85,7 @@ test('runCli passes the arguments untouched (no shell) and reads JSON', async ()
|
||||
|
||||
// ---------------------------------------------------------------------------------- native-control in packaged mode
|
||||
|
||||
function packagedFixture({ engineUsable = true, updater = null } = {}) {
|
||||
function packagedFixture({ engineUsable = true, updater = null, repair = false } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-packaged-test-'));
|
||||
const install = join(root, 'install'), data = join(root, 'data');
|
||||
mkdirSync(install, { recursive: true });
|
||||
@@ -112,6 +112,7 @@ function packagedFixture({ engineUsable = true, updater = null } = {}) {
|
||||
async saveProfile(name, path) { calls.push(['saveProfile', name, basename(path)]); saved.push(JSON.parse(readFileSync(path, 'utf8'))); return {}; },
|
||||
async start(name, options) { calls.push(['start', name, options?.waitSeconds]); if (startFailure && startFailureLeft-- > 0) { const e = new Error(startFailure.message); e.output = startFailure.output; throw e; } return { ok: true, state: 'ready' }; },
|
||||
async stop(name) { calls.push(['stop', name]); return { ok: true, state: 'stopped' }; },
|
||||
...(repair ? { async repairAccess() { calls.push(['repairAccess']); return { ok: true }; } } : {}),
|
||||
...(updater ? { async updateCheck() { calls.push(['updateCheck']); return updater.report; }, async update(options) { calls.push(['update', options.engine, options.control]); return updater.update(options); } } : {}),
|
||||
};
|
||||
const requests = [];
|
||||
@@ -472,3 +473,24 @@ test('packaged: a tune measures the engine a load starts: same start args, slots
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
}
|
||||
});
|
||||
|
||||
test('packaged: a profile an elevated run locked (PermissionError) is repaired once and the start is tried again; without repair it is a plain message, not a memory retry', async () => {
|
||||
const f = packagedFixture({ repair: true });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
f.failStart({ message: 'cannot read profile (PermissionError)' }, 1);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: 'iq2_xs' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.calls.filter(c => c[0] === 'repairAccess').length, 1);
|
||||
assert.equal(f.calls.filter(c => c[0] === 'start').length, 2);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
const g = packagedFixture({ repair: true });
|
||||
try {
|
||||
await g.native.refresh(true);
|
||||
g.failStart({ message: 'cannot read profile (PermissionError)' });
|
||||
const job = await finish(g.native, g.native.operation('load', { model: 'iq2_xs' }).id);
|
||||
assert.equal(job.state, 'error');
|
||||
assert.equal(g.calls.filter(c => c[0] === 'start').length, 2, 'one repair try, no memory retry on top');
|
||||
assert.match(job.error, /PermissionError/);
|
||||
} finally { await g.native.close(); g.cleanup(); }
|
||||
});
|
||||
@@ -7340,6 +7340,11 @@ def _component_exe(reference: str, cfg: dict) -> str:
|
||||
|
||||
|
||||
def main(argv=None, *, lifecycle_event=None, interrupt_exit_code: int = 0) -> int:
|
||||
try: # files made by a run as administrator still belong to the user
|
||||
from strata_control import private_files as _pf
|
||||
_pf.adopt_user_defaults()
|
||||
except Exception:
|
||||
pass
|
||||
event_sink = lifecycle_event
|
||||
|
||||
def notify_lifecycle(kind: str, payload: dict) -> None:
|
||||
|
||||
@@ -6108,6 +6108,11 @@ def preview_family(a) -> int:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try: # files made by a run as administrator still belong to the user
|
||||
from strata_control import private_files as _pf
|
||||
_pf.adopt_user_defaults()
|
||||
except Exception:
|
||||
pass
|
||||
if sys.platform == "darwin": # a Mac: metal/setup_mac.py (the Metal engine)
|
||||
return mac_setup(sys.argv[1:])
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
+7
-1
@@ -6,6 +6,12 @@ sampled from telemetry; missing signals stay unknown. It does not tune a model.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
except ImportError: # a thin bundle (monitor only) ships without strata_control
|
||||
def _private_mkdir(path, *, parents=False, exist_ok=True):
|
||||
path.mkdir(mode=0o700, parents=parents, exist_ok=exist_ok)
|
||||
return path
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
@@ -782,7 +788,7 @@ def run(endpoint: Endpoint, telemetry: Endpoint, out: Path, *, suite: str = "ser
|
||||
raise ProfileError(f"cancellation concurrency must cover all {slots} effective slots", 2)
|
||||
elif slots < 2 or concurrency < 2:
|
||||
raise ProfileError("prefill cancellation suite requires at least two effective slots and concurrency >= 2", 5)
|
||||
out.mkdir(mode=0o700, parents=False)
|
||||
_private_mkdir(out, parents=False, exist_ok=False)
|
||||
phases = []
|
||||
started = _utc_now()
|
||||
planned_phases = _planned_phase_kinds(suite, repeat)
|
||||
|
||||
@@ -104,6 +104,8 @@ def parser() -> argparse.ArgumentParser:
|
||||
profile_rollback.add_argument("--to", required=True, metavar="REVISION")
|
||||
profile_rollback.add_argument("--output", choices=("human", "json"), default="human")
|
||||
|
||||
repair = sp.add_parser("repair-access", help="give you back Strata files that a run as administrator locked (Windows)")
|
||||
repair.add_argument("--output", choices=("human", "json"), default="human")
|
||||
doctor = sp.add_parser("doctor", help="read-only check of installed engine components")
|
||||
doctor.add_argument("--profile", help="saved profile name or run config JSON path")
|
||||
doctor.add_argument("--components", action="store_true", help="include component inventory (default)")
|
||||
@@ -1782,12 +1784,43 @@ def _install(a: argparse.Namespace) -> int:
|
||||
cwd=str(installer.parent))
|
||||
|
||||
|
||||
def _access_retry(a: argparse.Namespace, error: BaseException) -> int | None:
|
||||
"""A file in Strata's own per-user folders that this user cannot open (a run as administrator made it) is repaired once
|
||||
and the command runs again; if that does not help the person gets one plain sentence, with the technical text after it."""
|
||||
try:
|
||||
from strata_control import private_files
|
||||
except ImportError:
|
||||
return None
|
||||
if not (isinstance(error, PermissionError) or "PermissionError" in str(error)):
|
||||
return None
|
||||
as_json = bool(getattr(a, "json", False) or getattr(a, "output", None) in ("json", "jsonl"))
|
||||
if getattr(a, "_access_retried", False):
|
||||
return _error(f"{private_files.ACCESS_MESSAGE} (details: {error})", 5, as_json)
|
||||
a._access_retried = True
|
||||
private_files.repair_known_roots()
|
||||
return _run(a)
|
||||
|
||||
|
||||
def _repair_access(a: argparse.Namespace) -> int:
|
||||
from strata_control import private_files
|
||||
report = private_files.repair_known_roots()
|
||||
if getattr(a, "output", "human") == "json":
|
||||
print(json.dumps({"ok": not report["failed"], **report}))
|
||||
else:
|
||||
print(f"checked {report['checked']} entries, repaired {report['repaired']}, could not repair {report['failed']}")
|
||||
if report["failed"]:
|
||||
print(private_files.ACCESS_MESSAGE)
|
||||
return 0 if not report["failed"] else 5
|
||||
|
||||
|
||||
def _run(a: argparse.Namespace) -> int:
|
||||
try:
|
||||
if a.command == "serve":
|
||||
return _serve(a)
|
||||
if a.command == "install":
|
||||
return _install(a)
|
||||
if a.command == "repair-access":
|
||||
return _repair_access(a)
|
||||
if a.command == "profile":
|
||||
return _profile_command(a)
|
||||
if a.command == "components" and a.components_command == "plan":
|
||||
@@ -1822,6 +1855,9 @@ def _run(a: argparse.Namespace) -> int:
|
||||
getattr(a, "output", None) in ("json", "jsonl"))
|
||||
return monitor_commands.run(a)
|
||||
except ValueError as e:
|
||||
retried = _access_retry(a, e)
|
||||
if retried is not None:
|
||||
return retried
|
||||
if a.command == "serve" and a.output == "jsonl":
|
||||
return _serve_error(a, str(e), getattr(e, "exit_code", 2))
|
||||
return _error(str(e), getattr(e, "exit_code", 2),
|
||||
@@ -1829,6 +1865,9 @@ def _run(a: argparse.Namespace) -> int:
|
||||
except BrokenPipeError:
|
||||
return 0
|
||||
except OSError as e:
|
||||
retried = _access_retry(a, e)
|
||||
if retried is not None:
|
||||
return retried
|
||||
if a.command == "serve" and a.output == "jsonl":
|
||||
return _serve_error(a, f"local file operation failed ({type(e).__name__})", 6)
|
||||
return _error(f"local file operation failed ({type(e).__name__})", 6,
|
||||
@@ -1867,6 +1906,11 @@ class _QuietCapture:
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
try:
|
||||
from strata_control import private_files
|
||||
private_files.adopt_user_defaults() # what this process creates belongs to the user even when it runs as administrator
|
||||
except ImportError: # a thin bundle ships without strata_control
|
||||
pass
|
||||
ap = parser()
|
||||
a = ap.parse_args(argv)
|
||||
if getattr(a, "json", False) and a.command not in ("status", "monitor", "logs", "connection", "config"):
|
||||
|
||||
@@ -6,6 +6,12 @@ CLI resolve the same saved connection without importing server or installer code
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
except ImportError: # a thin bundle (monitor only) ships without strata_control
|
||||
def _private_mkdir(path, *, parents=False, exist_ok=True):
|
||||
path.mkdir(mode=0o700, parents=parents, exist_ok=exist_ok)
|
||||
return path
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -61,7 +67,7 @@ def connection_dir(*, create: bool = False) -> Path:
|
||||
else:
|
||||
directory = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "strata" / "connections"
|
||||
if create:
|
||||
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
_private_mkdir(directory, parents=True, exist_ok=True)
|
||||
if directory.exists() or directory.is_symlink():
|
||||
details = directory.lstat()
|
||||
if not stat.S_ISDIR(details.st_mode) or (os.name == "posix" and
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
import hashlib
|
||||
import errno
|
||||
import os
|
||||
@@ -27,7 +28,7 @@ def _lease_file(profile: str, *, create: bool) -> Path | None:
|
||||
raise ProfileError("benchmark lease needs a saved profile")
|
||||
directory = profiles.profile_dir()
|
||||
if create:
|
||||
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
_private_mkdir(directory, parents=True, exist_ok=True)
|
||||
elif not directory.exists():
|
||||
return None
|
||||
details = directory.lstat()
|
||||
|
||||
@@ -7,6 +7,7 @@ hardware; applying it is a separate operation with a backup and smoke generation
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
@@ -452,7 +453,7 @@ def _backup_directory(path: Path, *, create: bool = False) -> Path:
|
||||
directory = path.parent / ".strata-calibration-backups"
|
||||
try:
|
||||
if create:
|
||||
directory.mkdir(mode=0o700, exist_ok=True)
|
||||
_private_mkdir(directory, exist_ok=True)
|
||||
details = directory.lstat()
|
||||
except FileNotFoundError:
|
||||
raise ProfileError("calibration backup directory is missing", 5) from None
|
||||
@@ -566,7 +567,7 @@ def _apply_locked(ref: str, path: Path, profile: dict | None, revision: str | No
|
||||
end_path, end_profile, end_revision, end_cfg = _target(ref)
|
||||
if end_path != path or _identity(end_path, end_cfg, end_profile, end_revision) != report["identity"]:
|
||||
raise ProfileError("engine, model or hardware changed during smoke generation; config unchanged", 6)
|
||||
settings_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
_private_mkdir(settings_path.parent, parents=True, exist_ok=True)
|
||||
current_settings = settings_path.read_bytes() if settings_path.exists() else None
|
||||
if current_settings != previous_settings:
|
||||
raise ProfileError("setup settings changed during calibration; config unchanged", 6)
|
||||
|
||||
@@ -7,6 +7,7 @@ crash has unknown completion; it is not proof that a worker is still running.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
@@ -45,7 +46,7 @@ DIRECT_KINDS = frozenset({"runtime.start", "runtime.stop", "runtime.restart", "r
|
||||
def _directory(profile: str, *, create: bool = False) -> Path:
|
||||
directory = recipes._directory(profile, create=create) / ".runs"
|
||||
if create:
|
||||
directory.mkdir(mode=0o700, exist_ok=True)
|
||||
_private_mkdir(directory, exist_ok=True)
|
||||
if directory.exists() or directory.is_symlink():
|
||||
details = directory.lstat()
|
||||
if not stat.S_ISDIR(details.st_mode):
|
||||
|
||||
@@ -41,11 +41,21 @@ if WINDOWS:
|
||||
_advapi.GetNamedSecurityInfoW.restype = wintypes.DWORD
|
||||
_advapi.ConvertSecurityDescriptorToStringSecurityDescriptorW.argtypes = [
|
||||
ctypes.c_void_p, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(wintypes.LPWSTR), ctypes.c_void_p]
|
||||
_advapi.ConvertStringSidToSidW.argtypes = [wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_void_p)]
|
||||
_advapi.SetTokenInformation.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
|
||||
_advapi.LookupPrivilegeValueW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, ctypes.c_void_p]
|
||||
_advapi.AdjustTokenPrivileges.argtypes = [wintypes.HANDLE, wintypes.BOOL, ctypes.c_void_p, wintypes.DWORD,
|
||||
ctypes.c_void_p, ctypes.c_void_p]
|
||||
|
||||
_SE_FILE_OBJECT = 1
|
||||
_DACL = 0x4
|
||||
_PROTECTED_DACL = 0x80000000
|
||||
_TOKEN_QUERY = 0x8
|
||||
_TOKEN_ADJUST_PRIVILEGES = 0x20
|
||||
_TOKEN_ADJUST_DEFAULT = 0x80
|
||||
_TOKEN_OWNER = 4
|
||||
_TOKEN_DEFAULT_DACL = 6
|
||||
_OWNER = 0x1
|
||||
_TOKEN_USER = 1
|
||||
_SDDL_REVISION_1 = 1
|
||||
_sid_cache: str | None = None
|
||||
@@ -109,8 +119,16 @@ def restrict_to_current_user(path: str | os.PathLike, *, directory: bool | None
|
||||
if not _advapi.GetSecurityDescriptorDacl(descriptor, ctypes.byref(present), ctypes.byref(dacl),
|
||||
ctypes.byref(defaulted)) or not present.value:
|
||||
raise _winerr("GetSecurityDescriptorDacl")
|
||||
code = _advapi.SetNamedSecurityInfoW(path, _SE_FILE_OBJECT, _DACL | _PROTECTED_DACL,
|
||||
None, None, dacl, None)
|
||||
# The explicit entry for the user's own SID is what makes the file reachable from a normal (non-elevated) session:
|
||||
# a file made by an elevated process is owned by BUILTIN\Administrators, and a rule naming only SYSTEM,
|
||||
# Administrators and OWNER RIGHTS shuts the same person out when UAC filters Administrators from their token.
|
||||
# So the owner becomes the user too, where Windows allows it; when it does not, the explicit entry still stands.
|
||||
owner = _user_psid()
|
||||
code = _advapi.SetNamedSecurityInfoW(path, _SE_FILE_OBJECT, _OWNER | _DACL | _PROTECTED_DACL,
|
||||
owner, None, dacl, None) if owner else 1
|
||||
if code:
|
||||
code = _advapi.SetNamedSecurityInfoW(path, _SE_FILE_OBJECT, _DACL | _PROTECTED_DACL,
|
||||
None, None, dacl, None)
|
||||
if code:
|
||||
raise _winerr(f"SetNamedSecurityInfoW {path}", code)
|
||||
finally:
|
||||
@@ -176,3 +194,270 @@ def is_unshared(path: str | os.PathLike) -> bool:
|
||||
if len(fields) >= 6 and fields[0] in ("A", "OA") and fields[5] not in allowed:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------- access repair
|
||||
|
||||
def _user_psid():
|
||||
"""The current user's SID as a native pointer (kept for the life of the process), or None."""
|
||||
global _psid_cache
|
||||
if not WINDOWS:
|
||||
return None
|
||||
if _psid_cache is not None:
|
||||
return _psid_cache
|
||||
pointer = ctypes.c_void_p()
|
||||
if not _advapi.ConvertStringSidToSidW(current_user_sid(), ctypes.byref(pointer)):
|
||||
return None
|
||||
_psid_cache = pointer.value
|
||||
return _psid_cache
|
||||
|
||||
|
||||
_psid_cache = None
|
||||
_keep: list = []
|
||||
|
||||
|
||||
def private_mkdir(path: str | os.PathLike, *, parents: bool = False, exist_ok: bool = True) -> Path:
|
||||
"""Create a folder only the user can reach, on every platform.
|
||||
|
||||
POSIX: ``mkdir(mode=0o700)``. Windows: Python 3.13 turns ``mode=0o700`` into an ACL of SYSTEM, Administrators and OWNER
|
||||
RIGHTS (inherited by everything inside), which locks the user out when an elevated process created the folder. This
|
||||
creates the missing folders plainly and gives each one the explicit entry for the user's own SID (and the user as owner).
|
||||
"""
|
||||
target = Path(path)
|
||||
if not WINDOWS:
|
||||
target.mkdir(mode=0o700, parents=parents, exist_ok=exist_ok)
|
||||
return target
|
||||
missing = []
|
||||
probe = target
|
||||
while not probe.exists() and probe != probe.parent:
|
||||
missing.append(probe)
|
||||
if not parents:
|
||||
break
|
||||
probe = probe.parent
|
||||
if not missing and not exist_ok:
|
||||
raise FileExistsError(str(target))
|
||||
for folder in reversed(missing):
|
||||
folder.mkdir(exist_ok=exist_ok)
|
||||
try:
|
||||
restrict_to_current_user(folder, directory=True)
|
||||
except OSError:
|
||||
pass # the folder exists with its inherited rule; a later read repairs it if that is not enough
|
||||
return target
|
||||
|
||||
|
||||
def adopt_user_defaults() -> bool:
|
||||
"""Make files this process creates belong to, and be open to, the user (Windows; no effect elsewhere).
|
||||
|
||||
An elevated process otherwise creates files owned by BUILTIN\\Administrators with a default rule of SYSTEM and
|
||||
Administrators only. Setting the token's default owner and default rule to the user's own SID fixes that for every file
|
||||
the process (and what it starts) creates afterwards. Returns True when both were set; never raises.
|
||||
"""
|
||||
if not WINDOWS:
|
||||
return False
|
||||
try:
|
||||
sid = current_user_sid()
|
||||
psid = _user_psid()
|
||||
token = wintypes.HANDLE()
|
||||
if not psid or not _advapi.OpenProcessToken(_kernel.GetCurrentProcess(), _TOKEN_QUERY | _TOKEN_ADJUST_DEFAULT,
|
||||
ctypes.byref(token)):
|
||||
return False
|
||||
try:
|
||||
class _Owner(ctypes.Structure):
|
||||
_fields_ = [("owner", ctypes.c_void_p)]
|
||||
|
||||
class _DefaultDacl(ctypes.Structure):
|
||||
_fields_ = [("dacl", ctypes.c_void_p)]
|
||||
|
||||
ok_owner = bool(_advapi.SetTokenInformation(token, _TOKEN_OWNER, ctypes.byref(_Owner(psid)),
|
||||
ctypes.sizeof(_Owner)))
|
||||
descriptor = ctypes.c_void_p()
|
||||
if not _advapi.ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
f"D:(A;;FA;;;{sid})(A;;FA;;;SY)(A;;FA;;;BA)", _SDDL_REVISION_1, ctypes.byref(descriptor), None):
|
||||
return False
|
||||
present, defaulted, dacl = wintypes.BOOL(), wintypes.BOOL(), ctypes.c_void_p()
|
||||
if not _advapi.GetSecurityDescriptorDacl(descriptor, ctypes.byref(present), ctypes.byref(dacl),
|
||||
ctypes.byref(defaulted)) or not present.value:
|
||||
return False
|
||||
_keep.append(descriptor) # the token keeps pointing into it
|
||||
ok_dacl = bool(_advapi.SetTokenInformation(token, _TOKEN_DEFAULT_DACL, ctypes.byref(_DefaultDacl(dacl.value)),
|
||||
ctypes.sizeof(_DefaultDacl)))
|
||||
return ok_owner and ok_dacl
|
||||
finally:
|
||||
_kernel.CloseHandle(token)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _enable_privilege(name: str) -> bool:
|
||||
token = wintypes.HANDLE()
|
||||
if not _advapi.OpenProcessToken(_kernel.GetCurrentProcess(), _TOKEN_QUERY | _TOKEN_ADJUST_PRIVILEGES,
|
||||
ctypes.byref(token)):
|
||||
return False
|
||||
try:
|
||||
class _Luid(ctypes.Structure):
|
||||
_fields_ = [("low", wintypes.DWORD), ("high", ctypes.c_long)]
|
||||
|
||||
class _Privileges(ctypes.Structure):
|
||||
_fields_ = [("count", wintypes.DWORD), ("luid", _Luid), ("attributes", wintypes.DWORD)]
|
||||
|
||||
luid = _Luid()
|
||||
if not _advapi.LookupPrivilegeValueW(None, name, ctypes.byref(luid)):
|
||||
return False
|
||||
privileges = _Privileges(1, luid, 2) # SE_PRIVILEGE_ENABLED
|
||||
ctypes.set_last_error(0)
|
||||
_advapi.AdjustTokenPrivileges(token, False, ctypes.byref(privileges), ctypes.sizeof(privileges), None, None)
|
||||
return ctypes.get_last_error() == 0
|
||||
finally:
|
||||
_kernel.CloseHandle(token)
|
||||
|
||||
|
||||
def user_roots() -> list[Path]:
|
||||
"""The folders that are the user's own per-user data: a file inside one may be repaired by that user."""
|
||||
roots: list[Path] = []
|
||||
for key in ("APPDATA", "LOCALAPPDATA", "STRATA_HOME", "STRATA_PROFILE_DIR", "STRATA_INSTALL_DIR", "STRATA_RUNTIME_DIR",
|
||||
"STRATA_STATE_DIR"):
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
roots.append(Path(value))
|
||||
roots.append(Path.home())
|
||||
result = []
|
||||
for root in roots:
|
||||
try:
|
||||
result.append(root.resolve())
|
||||
except OSError:
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def is_users_own(path: str | os.PathLike) -> bool:
|
||||
"""True when ``path`` is inside the user's own profile or data folders (never the system folders)."""
|
||||
try:
|
||||
target = Path(path).resolve()
|
||||
except OSError:
|
||||
return False
|
||||
return any(target == root or root in target.parents for root in user_roots())
|
||||
|
||||
|
||||
def repair_access(path: str | os.PathLike, *, tree: bool = True) -> bool:
|
||||
"""Give the current user back a file or folder in their own data folders that an elevated process locked them out of.
|
||||
|
||||
Takes ownership and writes the explicit protected entry for the user's SID (a folder's children are repaired too).
|
||||
Needs the right to do that: an elevated session, or a user holding the take-ownership right; a normal session locked
|
||||
out by someone else's elevated process cannot do it, and this returns False (the caller shows the plain message).
|
||||
Windows only; elsewhere there is nothing to repair. Refuses anything outside the user's own folders.
|
||||
"""
|
||||
if not WINDOWS or not is_users_own(path):
|
||||
return False
|
||||
_enable_privilege("SeTakeOwnershipPrivilege")
|
||||
_enable_privilege("SeRestorePrivilege")
|
||||
fixed = True
|
||||
target = Path(path)
|
||||
try:
|
||||
restrict_to_current_user(target, directory=target.is_dir())
|
||||
except OSError:
|
||||
fixed = False
|
||||
if tree and fixed and target.is_dir():
|
||||
try:
|
||||
for dirpath, dirnames, filenames in os.walk(target):
|
||||
for name in dirnames + filenames:
|
||||
child = os.path.join(dirpath, name)
|
||||
if os.path.islink(child):
|
||||
continue
|
||||
try:
|
||||
restrict_to_current_user(child, directory=os.path.isdir(child))
|
||||
except OSError:
|
||||
fixed = False
|
||||
except OSError:
|
||||
fixed = False
|
||||
return fixed
|
||||
|
||||
|
||||
def read_bytes_repairing(path: str | os.PathLike) -> bytes:
|
||||
"""``Path.read_bytes`` that, on an access error in the user's own folders, repairs the access once and tries again."""
|
||||
target = Path(path)
|
||||
try:
|
||||
return target.read_bytes()
|
||||
except PermissionError:
|
||||
if not repair_access(target, tree=False):
|
||||
raise
|
||||
return target.read_bytes()
|
||||
|
||||
|
||||
ACCESS_MESSAGE = "Strata can't open its settings file. Run Strata once as yourself (not as administrator) or press Repair."
|
||||
|
||||
|
||||
_SKIP_DIRS = {"models", "node_modules", "harness", "python", "host", "node", "control", "__pycache__", ".git"}
|
||||
_SKIP_SUFFIXES = (".gguf", ".part", ".bin", ".safetensors", ".zip", ".exe", ".dll")
|
||||
|
||||
|
||||
def known_roots() -> list[Path]:
|
||||
"""Strata's per-user data folders: APPDATA/Strata, LOCALAPPDATA/Strata and ~/.strata (plus the STRATA_* overrides)."""
|
||||
roots: list[Path] = []
|
||||
for key, sub in (("APPDATA", "Strata"), ("LOCALAPPDATA", "Strata")):
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
roots.append(Path(value) / sub)
|
||||
roots.append(Path.home() / ".strata")
|
||||
for key in ("STRATA_HOME", "STRATA_PROFILE_DIR", "STRATA_RUNTIME_DIR", "STRATA_STATE_DIR", "STRATA_INSTALL_DIR"):
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
roots.append(Path(value))
|
||||
seen, result = set(), []
|
||||
for root in roots:
|
||||
key = str(root).lower()
|
||||
if key not in seen and root.exists():
|
||||
seen.add(key)
|
||||
result.append(root)
|
||||
return result
|
||||
|
||||
|
||||
def repair_known_roots(*, limit: int = 20000) -> dict:
|
||||
"""Walk Strata's own per-user folders and repair every entry the user cannot open (Windows).
|
||||
|
||||
Meant to run when Strata starts: files and folders an elevated run created (owner Administrators, a rule that names
|
||||
only SYSTEM, Administrators and OWNER RIGHTS) are given back to the user. A normal session can only repair what its
|
||||
own token may change; an elevated start repairs everything. Never raises; returns what it did.
|
||||
"""
|
||||
report = {"checked": 0, "repaired": 0, "failed": 0}
|
||||
if not WINDOWS:
|
||||
return report
|
||||
for root in known_roots():
|
||||
stack = [root]
|
||||
while stack and report["checked"] < limit:
|
||||
folder = stack.pop()
|
||||
try:
|
||||
entries = list(os.scandir(folder))
|
||||
except PermissionError:
|
||||
report["checked"] += 1
|
||||
if repair_access(folder, tree=False):
|
||||
report["repaired"] += 1
|
||||
try:
|
||||
entries = list(os.scandir(folder))
|
||||
except OSError:
|
||||
continue
|
||||
else:
|
||||
report["failed"] += 1
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
for entry in entries:
|
||||
report["checked"] += 1
|
||||
try:
|
||||
if entry.is_symlink():
|
||||
continue
|
||||
if entry.is_dir(follow_symlinks=False):
|
||||
if entry.name.lower() not in _SKIP_DIRS:
|
||||
stack.append(Path(entry.path))
|
||||
continue
|
||||
if entry.name.lower().endswith(_SKIP_SUFFIXES):
|
||||
continue
|
||||
with open(entry.path, "rb"):
|
||||
pass
|
||||
except PermissionError:
|
||||
if repair_access(entry.path, tree=False):
|
||||
report["repaired"] += 1
|
||||
else:
|
||||
report["failed"] += 1
|
||||
except OSError:
|
||||
continue
|
||||
return report
|
||||
@@ -19,6 +19,7 @@ exactly the engine executable that is about to be started, running ``--serve`` w
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
@@ -167,7 +168,7 @@ def procs_dir(create: bool = True) -> Path:
|
||||
"""The owner-private folder of launch records (beside runtime/, under the per-user state)."""
|
||||
base = paths.state_base() / "procs"
|
||||
if create and not base.exists():
|
||||
base.mkdir(parents=True, mode=0o700, exist_ok=True)
|
||||
_private_mkdir(base, parents=True, exist_ok=True)
|
||||
if WINDOWS:
|
||||
from strata_control import private_files
|
||||
private_files.restrict_to_current_user(base, directory=True)
|
||||
|
||||
@@ -7,6 +7,7 @@ them. It deliberately does not start processes or contact a network service.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
@@ -150,7 +151,8 @@ def load(name: str) -> tuple[dict, str]:
|
||||
try:
|
||||
if not stat.S_ISREG(path.lstat().st_mode):
|
||||
raise ProfileError("profile must be a regular file", 5)
|
||||
data = path.read_bytes()
|
||||
from strata_control import private_files
|
||||
data = private_files.read_bytes_repairing(path) # a profile an elevated run locked is repaired once, in place
|
||||
if len(data) > 1024 * 1024:
|
||||
raise ProfileError("profile is larger than 1 MiB", 5)
|
||||
value = json.loads(data.decode("utf-8-sig"))
|
||||
@@ -182,7 +184,7 @@ def list_profiles() -> list[dict]:
|
||||
@contextmanager
|
||||
def _lock(name: str):
|
||||
directory = profile_dir()
|
||||
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
_private_mkdir(directory, parents=True, exist_ok=True)
|
||||
lock = directory / f".{_name(name)}.lock"
|
||||
with open(lock, "a+b") as stream:
|
||||
if os.name == "nt":
|
||||
@@ -281,7 +283,7 @@ def save(name: str, proposed: dict, *, expected_revision: str,
|
||||
if current == proposed:
|
||||
return current, revision
|
||||
history = profile_dir() / ".history" / name
|
||||
history.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
_private_mkdir(history, parents=True, exist_ok=True)
|
||||
old = _path(name).read_bytes()
|
||||
if _revision(old) != revision:
|
||||
raise ProfileError("profile changed during edit; reload it before saving", 6)
|
||||
|
||||
@@ -7,6 +7,7 @@ schema and operation authority rather than add a separate implementation.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -46,8 +47,8 @@ def _directory(profile: str, *, create: bool = False) -> Path:
|
||||
if os.name == "posix" and (details.st_uid != os.getuid() or stat.S_IMODE(details.st_mode) & 0o077):
|
||||
raise ProfileError("recipe directory must be owner-only (mode 0700)", 5)
|
||||
if create:
|
||||
directory.parent.mkdir(mode=0o700, exist_ok=True)
|
||||
directory.mkdir(mode=0o700, exist_ok=True)
|
||||
_private_mkdir(directory.parent, exist_ok=True)
|
||||
_private_mkdir(directory, exist_ok=True)
|
||||
if directory.exists() or directory.is_symlink():
|
||||
details = directory.lstat()
|
||||
if not stat.S_ISDIR(details.st_mode):
|
||||
@@ -73,8 +74,8 @@ def _history_dir(profile: str, name: str, *, create: bool = False) -> Path:
|
||||
if path.is_symlink():
|
||||
raise ProfileError("recipe history directory must not be a symlink", 5)
|
||||
if create:
|
||||
root.mkdir(mode=0o700, exist_ok=True)
|
||||
directory.mkdir(mode=0o700, exist_ok=True)
|
||||
_private_mkdir(root, exist_ok=True)
|
||||
_private_mkdir(directory, exist_ok=True)
|
||||
for path in (root, directory):
|
||||
if path.exists():
|
||||
details = path.lstat()
|
||||
@@ -411,7 +412,7 @@ def run(profile: str, name: str, *, exclusive_ack: bool = False,
|
||||
elif kind == "engine.calibrate.measure":
|
||||
from strata_control import calibration
|
||||
directory = Path(recipe["params"]["report_dir"])
|
||||
directory.mkdir(mode=0o700, exist_ok=True)
|
||||
_private_mkdir(directory, exist_ok=True)
|
||||
if not stat.S_ISDIR(directory.lstat().st_mode):
|
||||
raise ProfileError("report_dir must be a directory, not a symlink", 5)
|
||||
report_path = directory / f"calibration-{uuid.uuid4().hex}.json"
|
||||
|
||||
@@ -15,6 +15,7 @@ child is still there.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strata_control.private_files import private_mkdir as _private_mkdir
|
||||
import argparse
|
||||
import errno
|
||||
import hashlib
|
||||
@@ -107,7 +108,7 @@ def runtime_dir() -> Path:
|
||||
if not base.is_absolute():
|
||||
raise ProfileError("STRATA_RUNTIME_DIR must be absolute")
|
||||
existed = base.exists()
|
||||
base.mkdir(parents=True, mode=0o700, exist_ok=True)
|
||||
_private_mkdir(base, parents=True, exist_ok=True)
|
||||
if WINDOWS and not existed:
|
||||
private_files.restrict_to_current_user(base, directory=True) # no mode bits on NTFS: a protected ACL
|
||||
details = base.lstat()
|
||||
|
||||
@@ -45,6 +45,7 @@ NETWORK_ALLOWED = {
|
||||
("serve/server.py", "urlopen"): "an image_url a client sends in a chat request is fetched at request time",
|
||||
("serve/mcp.py", "urlopen"): "the MCP servers the user configured, at request time",
|
||||
("strata_control/tune_api.py", "urlopen"): "the loopback engine/server of a tuning job the user started",
|
||||
("strata_control/tune_session.py", "urlopen"): "the loopback engine (127.0.0.1) the auto-tuner a user started measures",
|
||||
("strata_control/components.py", "urlopen"): "the component fetcher: only `strata install`/UPDATE call it",
|
||||
}
|
||||
#: Modules that import the installer on purpose, with the explicit command that reaches them.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
@@ -94,3 +95,88 @@ class PrivateFileTests(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
@unittest.skipUnless(pf.WINDOWS, "NTFS ACLs")
|
||||
class ElevatedRunLockoutTests(unittest.TestCase):
|
||||
"""A file or folder an elevated run made names only SYSTEM and Administrators: the user's normal session cannot open it."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
|
||||
@staticmethod
|
||||
def lock_out(path):
|
||||
"""Replace the rule with SYSTEM only: this session may be elevated itself, so Administrators is left out to make the lock-out real."""
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
descriptor = ctypes.c_void_p()
|
||||
assert pf._advapi.ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
"D:P(A;OICI;FA;;;SY)", 1, ctypes.byref(descriptor), None)
|
||||
present, defaulted, dacl = wintypes.BOOL(), wintypes.BOOL(), ctypes.c_void_p()
|
||||
pf._advapi.GetSecurityDescriptorDacl(descriptor, ctypes.byref(present), ctypes.byref(dacl), ctypes.byref(defaulted))
|
||||
code = pf._advapi.SetNamedSecurityInfoW(str(path), 1, 0x4 | 0x80000000, None, None, dacl, None)
|
||||
assert code == 0, code
|
||||
|
||||
def test_a_locked_file_is_read_again_after_the_repair(self):
|
||||
path = self.root / "profile.json"
|
||||
path.write_text('{"name": "x"}')
|
||||
self.lock_out(path)
|
||||
with self.assertRaises(PermissionError):
|
||||
path.read_bytes()
|
||||
self.assertEqual(pf.read_bytes_repairing(path), b'{"name": "x"}')
|
||||
self.assertIn(pf.current_user_sid(), pf.dacl_sddl(path))
|
||||
self.assertEqual(path.read_bytes(), b'{"name": "x"}')
|
||||
|
||||
def test_a_locked_folder_and_what_is_inside_are_repaired_as_a_tree(self):
|
||||
folder = self.root / "profiles"
|
||||
folder.mkdir()
|
||||
(folder / "a.json").write_text("{}")
|
||||
(folder / "sub").mkdir()
|
||||
(folder / "sub" / "b.json").write_text("{}")
|
||||
for item in (folder / "sub" / "b.json", folder / "sub", folder / "a.json", folder):
|
||||
self.lock_out(item)
|
||||
with self.assertRaises(PermissionError):
|
||||
(folder / "a.json").read_bytes()
|
||||
os.environ["STRATA_HOME"] = str(self.root)
|
||||
self.addCleanup(os.environ.pop, "STRATA_HOME", None)
|
||||
report = pf.repair_known_roots()
|
||||
self.assertGreaterEqual(report["repaired"], 1)
|
||||
self.assertEqual(report["failed"], 0)
|
||||
self.assertEqual((folder / "a.json").read_bytes(), b"{}")
|
||||
self.assertEqual((folder / "sub" / "b.json").read_bytes(), b"{}")
|
||||
self.assertIn(pf.current_user_sid(), pf.dacl_sddl(folder / "sub" / "b.json"))
|
||||
|
||||
def test_a_folder_made_with_private_mkdir_names_the_user_explicitly(self):
|
||||
made = pf.private_mkdir(self.root / "x" / "y", parents=True)
|
||||
self.assertIn(pf.current_user_sid(), pf.dacl_sddl(made))
|
||||
self.assertIn(pf.current_user_sid(), pf.dacl_sddl(made.parent))
|
||||
(made / "file.json").write_text("{}")
|
||||
self.assertIn(pf.current_user_sid(), pf.dacl_sddl(made / "file.json"))
|
||||
self.assertEqual((made / "file.json").read_text(), "{}")
|
||||
|
||||
def test_the_process_defaults_can_be_set_for_the_user(self):
|
||||
self.assertTrue(pf.adopt_user_defaults())
|
||||
self.assertTrue(pf.adopt_user_defaults()) # again: nothing breaks
|
||||
path = self.root / "new.json"
|
||||
path.write_text("{}")
|
||||
self.assertEqual(path.read_text(), "{}")
|
||||
|
||||
def test_only_the_users_own_folders_are_ever_repaired(self):
|
||||
self.assertFalse(pf.repair_access(os.environ.get("SystemRoot", "C:\Windows") + "\System32\drivers"))
|
||||
|
||||
def test_a_profile_that_was_locked_loads(self):
|
||||
from strata_control import profiles
|
||||
os.environ["STRATA_PROFILE_DIR"] = str(self.root / "profiles")
|
||||
self.addCleanup(os.environ.pop, "STRATA_PROFILE_DIR", None)
|
||||
directory = self.root / "profiles"
|
||||
directory.mkdir()
|
||||
run_config = self.root / "strata-x.json"
|
||||
run_config.write_text("{}")
|
||||
(directory / "app-1.json").write_text(json.dumps({"schema": 1, "name": "app-1", "source_config": str(run_config)}))
|
||||
self.lock_out(directory / "app-1.json")
|
||||
try:
|
||||
profiles.load("app-1")
|
||||
except profiles.ProfileError as exc:
|
||||
self.assertNotIn("PermissionError", str(exc)) # any other complaint is the sample's shape, not access
|
||||
Reference in new issue
Block a user