| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- """Install a sealed enterprise workshop without copying credentials or identities."""
- import argparse
- import hashlib
- import json
- import os
- from pathlib import Path, PurePosixPath
- import re
- import shutil
- import sys
- import tempfile
- import uuid
- sys.dont_write_bytecode = True
- HERE = Path(__file__).absolute().parent
- COMPANY = 'FKieRbIoGt'
- APP = 'FKieRbIoGt_main'
- class Stopped(Exception):
- pass
- def need(value, code):
- if not value:
- raise Stopped(code)
- def sha(raw):
- return hashlib.sha256(raw).hexdigest()
- def encode(value):
- return (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
- def plain(value):
- p = Path(os.path.abspath(value))
- for part in [*reversed(p.parents), p]:
- need(not part.is_symlink(), 'SYMLINK_PATH')
- need(not getattr(part, 'is_junction', lambda: False)(), 'JUNCTION_PATH')
- return p
- def safe_name(name):
- return (isinstance(name, str) and bool(name) and '\\' not in name and ':' not in name
- and not name.startswith('/') and name == '/'.join(PurePosixPath(name).parts)
- and all(x not in ('.', '..') for x in PurePosixPath(name).parts))
- def private_json(file):
- file = plain(file)
- need(file.is_file(), 'PRIVATE_FILE_MISSING')
- stat = file.stat()
- if os.name != 'nt':
- need(stat.st_uid == os.geteuid() and stat.st_mode & 0o077 == 0, 'PRIVATE_FILE_PERMISSIONS')
- return json.loads(file.read_bytes())
- def sealed(root):
- root = plain(root)
- listed = {}
- for line in (root / 'FILES.sha256').read_text('utf-8').splitlines():
- m = re.fullmatch(r'([a-f0-9]{64}) (.+)', line)
- need(m is not None, 'INVALID_MANIFEST')
- digest, name = m.groups()
- need(safe_name(name) and name not in listed, 'INVALID_MANIFEST_PATH')
- p = plain(root / name)
- need(p.is_file() and sha(p.read_bytes()) == digest, 'PACKAGE_HASH_MISMATCH')
- listed[name] = digest
- actual = set()
- for p in root.rglob('*'):
- if p.relative_to(root).parts[0] == '.git':
- continue
- plain(p)
- need(p.is_file() or p.is_dir(), 'SPECIAL_PACKAGE_FILE')
- if p.is_file():
- actual.add(p.relative_to(root).as_posix())
- need(actual == set(listed) | {'FILES.sha256'}, 'UNDECLARED_PACKAGE_FILE')
- cfg = json.loads((root / 'enterprise.json').read_bytes())
- need(cfg['schema'] == 'ksf.enterprise-workshop.v1' and cfg['companyObjectId'] == COMPANY
- and cfg['appId'] == APP, 'PACKAGE_ENTERPRISE_MISMATCH')
- need(cfg['functionsURL'] == 'https://kangshifu.fmode.cn/api/functions'
- and cfg['serverURL'] == 'https://kangshifu.fmode.cn/data', 'PACKAGE_TARGET_MISMATCH')
- entry = 'payload/skills/ksf-workshop-runtime/SKILL.md'
- need(listed.get(entry) == cfg['skill']['entrySha256'], 'SKILL_ENTRY_MISMATCH')
- return cfg, listed
- def candidate(root, listed):
- files = {}
- for name in listed:
- if name.startswith('payload/'):
- relative = name[len('payload/'):]
- need(relative.startswith(('skills/ksf-workshop-runtime/', 'skills/ksf-voc-library/scripts/')),
- 'UNEXPECTED_PAYLOAD')
- files[relative] = (root / name).read_bytes()
- skill = {n: b for n, b in files.items() if n.startswith('skills/ksf-workshop-runtime/')}
- need(len(skill) == 45 and len(files) > 45, 'INCOMPLETE_PAYLOAD')
- need(not any('credential-binding' in n or 'channel.json' in n for n in files), 'PRIVATE_PAYLOAD')
- return files
- def check_enterprise(current, expected):
- need(isinstance(current, dict), 'INVALID_ENTERPRISE_CONFIGURATION')
- need(current.get('companyObjectId') == expected['companyObjectId']
- and current.get('appId') == expected['appId'], 'ENTERPRISE_MISMATCH')
- need(all(isinstance(current.get(k), str) and current[k].rstrip('/') == expected[k]
- for k in ('serverURL', 'functionsURL')),
- 'ENTERPRISE_ENDPOINT_MISMATCH')
- need(current.get('provisionStatus') == 'success', 'ENTERPRISE_NOT_PROVISIONED')
- def tree_snapshot(directory):
- if not directory.exists():
- return None
- need(directory.is_dir(), 'NOT_DIRECTORY')
- result = {}
- for p in directory.rglob('*'):
- plain(p)
- need(p.is_file() or p.is_dir(), 'SPECIAL_TARGET_FILE')
- if p.is_file():
- result[p.relative_to(directory).as_posix()] = [sha(p.read_bytes()), p.stat().st_mode & 0o777]
- return result
- def plan(root, data_dir):
- root, data_dir = plain(root), plain(data_dir)
- need(data_dir.is_absolute() and data_dir != Path(data_dir.anchor), 'INVALID_DATA_DIR')
- need(root != data_dir and root not in data_dir.parents
- and not any(target == root or target in root.parents or root in target.parents
- for target in (data_dir / 'skills', data_dir / '.fmode',
- data_dir / 'ksf-enterprise-workshop')),
- 'PACKAGE_TARGET_OVERLAP')
- cfg, listed = sealed(root)
- current_path = data_dir / '.fmode/config/enterprise/current.json'
- current = private_json(current_path)
- check_enterprise(current, cfg)
- files = candidate(root, listed)
- targets = sorted({n.split('/')[1] for n in files})
- if (data_dir / 'skills/ksf-voc-library/SKILL.md').exists():
- raise Stopped('RESEARCH_SKILL_ENTRYPOINT_STILL_ACTIVE')
- snapshots = {n: tree_snapshot(plain(data_dir / 'skills' / n)) for n in targets}
- changes = []
- for name, raw in files.items():
- dest = plain(data_dir / name)
- need(not dest.exists() or dest.is_file(), 'TARGET_NOT_FILE')
- if dest.exists():
- need(sha(dest.read_bytes()) == sha(raw), 'EXISTING_COMPONENT_DIFFERS')
- else:
- # Existing directories may carry private local files, but cannot be a partial/unknown version.
- need(snapshots[name.split('/')[1]] is None, 'INCOMPLETE_EXISTING_COMPONENT')
- changes.append(name)
- binding = encode({'schema': 'ksf.enterprise-workshop-binding.v1', 'companyObjectId': COMPANY,
- 'appId': APP, 'configurationSha256': sha((root / 'enterprise.json').read_bytes()),
- 'packageManifestSha256': sha((root / 'FILES.sha256').read_bytes()),
- 'skill': 'ksf-workshop-runtime', 'version': 'r13'})
- binding_path = plain(data_dir / 'ksf-enterprise-workshop/binding.json')
- binding_before = sha(binding_path.read_bytes()) if binding_path.exists() else None
- if binding_path.exists():
- prior = json.loads((root / 'previous-release.json').read_bytes())
- need(binding_before in (sha(binding), prior['previousBindingSha256']), 'ENTERPRISE_BINDING_CHANGED')
- return {'schema': 'ksf.enterprise-workshop-plan.v1', 'companyObjectId': COMPANY, 'appId': APP,
- 'dataDir': str(data_dir), 'packageManifestSha256': sha((root / 'FILES.sha256').read_bytes()),
- 'currentPublicFields': {k: current[k] for k in ('companyObjectId', 'appId', 'serverURL', 'functionsURL')},
- 'snapshots': snapshots, 'changes': changes, 'bindingExists': binding_path.exists(),
- 'bindingSha256': sha(binding), 'bindingBeforeSha256': binding_before,
- 'bindingUpdateRequired': binding_before != sha(binding), 'businessWrites': 0}
- def atomic_create(file, raw, mode=0o600):
- plain(file)
- file.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
- with file.open('xb') as stream:
- os.chmod(file, mode)
- stream.write(raw)
- stream.flush()
- os.fsync(stream.fileno())
- def apply(root, data_dir, expected_plan):
- root, data_dir = plain(root), plain(data_dir)
- observed = plan(root, data_dir)
- if not observed['changes'] and observed['bindingExists'] and not observed['bindingUpdateRequired']:
- need(observed['packageManifestSha256'] == expected_plan['packageManifestSha256']
- and observed['dataDir'] == expected_plan['dataDir']
- and observed['currentPublicFields'] == expected_plan['currentPublicFields'], 'PLAN_CHANGED')
- return {**verify(root, data_dir), 'installedFiles': 0, 'applied': False}
- need(observed == expected_plan, 'PLAN_CHANGED')
- _, listed = sealed(root)
- files = candidate(root, listed)
- lock = plain(data_dir / '.ksf-enterprise-bootstrap.lock')
- created = []
- created_snapshots = {}
- replaced_binding = None
- descriptor = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
- os.close(descriptor)
- try:
- need(plan(root, data_dir) == expected_plan, 'PLAN_CHANGED')
- # Existing components are verify-only. All new component trees are staged before any rename.
- with tempfile.TemporaryDirectory(prefix='.ksf-enterprise-stage-', dir=data_dir) as tmp:
- staging = Path(tmp)
- for relative in expected_plan['changes']:
- atomic_create(staging / relative, files[relative], 0o644)
- for name, before in expected_plan['snapshots'].items():
- target = plain(data_dir / 'skills' / name)
- need(tree_snapshot(target) == before, 'CONCURRENT_INSTALL')
- if before is None:
- target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
- created_snapshots[target] = tree_snapshot(staging / 'skills' / name)
- os.rename(staging / 'skills' / name, target)
- created.append(target)
- bind = plain(data_dir / 'ksf-enterprise-workshop/binding.json')
- if expected_plan['bindingUpdateRequired']:
- raw = encode({'schema': 'ksf.enterprise-workshop-binding.v1', 'companyObjectId': COMPANY,
- 'appId': APP, 'configurationSha256': sha((root / 'enterprise.json').read_bytes()),
- 'packageManifestSha256': expected_plan['packageManifestSha256'],
- 'skill': 'ksf-workshop-runtime', 'version': 'r13'})
- if not bind.exists():
- atomic_create(bind, raw)
- created.append(bind)
- else:
- before = bind.read_bytes()
- need(sha(before) == expected_plan['bindingBeforeSha256'], 'CONCURRENT_INSTALL')
- atomic_create(bind.with_name('binding-before-' + uuid.uuid4().hex + '.json'), before)
- temp_bind = bind.with_name('.binding-' + uuid.uuid4().hex)
- atomic_create(temp_bind, raw)
- try:
- need(bind.read_bytes() == before, 'CONCURRENT_INSTALL')
- os.replace(temp_bind, bind)
- replaced_binding = (bind, before)
- finally:
- if temp_bind.exists():
- temp_bind.unlink()
- result = verify(root, data_dir)
- return {**result, 'installedFiles': len(expected_plan['changes']), 'applied': True,
- 'existingComponentsModified': False, 'identitiesCopied': False,
- 'credentialsCopied': False, 'vocModified': False}
- except Exception:
- if replaced_binding:
- bind, before = replaced_binding
- if sha(bind.read_bytes()) == expected_plan['bindingSha256']:
- rollback = bind.with_name('.binding-rollback-' + uuid.uuid4().hex)
- atomic_create(rollback, before)
- os.replace(rollback, bind)
- # Only remove newly created, still-identical payload files. Preserve concurrent changes.
- for p in reversed(created):
- if p.is_file():
- if sha(p.read_bytes()) == expected_plan['bindingSha256']:
- p.unlink()
- elif p.is_dir() and tree_snapshot(p) == created_snapshots[p]:
- need(p.parent == data_dir / 'skills', 'ROLLBACK_PATH_OUTSIDE_TARGET')
- shutil.rmtree(p)
- raise
- finally:
- lock.unlink()
- def verify(root, data_dir):
- result = plan(root, data_dir)
- need(not result['changes'] and result['bindingExists'] and not result['bindingUpdateRequired'],
- 'INSTALLATION_INCOMPLETE')
- return {'ok': True, 'companyObjectId': COMPANY, 'appId': APP, 'version': 'r13',
- 'filesVerified': len(candidate(plain(root), sealed(root)[1])), 'businessWrites': 0,
- 'agentLoaded': False, 'researchAuthorized': False, 'businessAuthorized': False,
- 'participantBound': False, 'automaticLifecycleHookVerified': False}
- def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('command', choices=['plan', 'apply', 'verify'])
- parser.add_argument('--data-dir', required=True, type=Path)
- parser.add_argument('--out', required=True, type=Path)
- args = parser.parse_args()
- need(args.data_dir.is_absolute() and args.out.is_absolute(), 'ABSOLUTE_PATH_REQUIRED')
- data_dir, out = plain(args.data_dir), plain(args.out)
- need(out != data_dir and out not in data_dir.parents and not (data_dir / 'skills') in out.parents,
- 'UNSAFE_RESULT_DIRECTORY')
- out.mkdir(parents=True, exist_ok=True, mode=0o700)
- if args.command == 'plan':
- record = plan(HERE, data_dir)
- atomic_create(out / 'plan.json', encode(record))
- result = {'ok': True, 'readOnly': True, 'changes': len(record['changes']), 'businessWrites': 0}
- elif args.command == 'apply':
- result = apply(HERE, data_dir, private_json(out / 'plan.json'))
- atomic_create(out / ('apply-' + uuid.uuid4().hex + '.json'), encode(result))
- else:
- result = verify(HERE, data_dir)
- atomic_create(out / ('verify-' + uuid.uuid4().hex + '.json'), encode(result))
- print(json.dumps(result, ensure_ascii=False, indent=2))
- if __name__ == '__main__':
- try:
- main()
- except (Stopped, OSError, ValueError, KeyError) as error:
- print(json.dumps({'ok': False, 'error': {'code': str(error) if isinstance(error, Stopped)
- else 'BOOTSTRAP_STOPPED'}, 'businessWrites': 0}), file=sys.stderr)
- sys.exit(1)
|