| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- """No-argument in-process registration, composed with the reviewed aibot bridge.
- No identity JSON is accepted from model tool arguments. The Node worker reuses
- the existing Skill key/profile code; its stdin is private transport, not proof.
- This does not isolate arbitrary same-UID terminal code from local state.
- """
- from __future__ import annotations
- import hashlib
- import json
- import os
- from pathlib import Path
- import re
- import shutil
- import stat
- import subprocess
- TOOL_NAME = 'ksf_register_current_employee'
- WORKER = Path('/opt/data/skills/ksf-workshop-runtime/scripts/ksf-identity-bridge.mjs')
- TOOL_SCHEMA = {
- 'name': TOOL_NAME,
- 'description': '登记当前企微消息发送者,返回员工档案键。无需参数;从本回合可信上下文读取,失败停止,不接受身份转述。只写本地档案,不建会话或写业务库。',
- 'parameters': {'type': 'object', 'properties': {}, 'additionalProperties': False},
- }
- class RegistrationError(Exception):
- pass
- def _check_private_file(path):
- # Permissions are defensive checks, not a same-UID provenance guarantee.
- for parent in (path, *path.parents):
- if parent.is_symlink():
- raise RegistrationError('REGISTRATION_CONFIG_UNSAFE')
- s = path.lstat()
- if not stat.S_ISREG(s.st_mode):
- raise RegistrationError('REGISTRATION_CONFIG_UNSAFE')
- if os.name == 'posix' and (s.st_uid != os.geteuid() or stat.S_IMODE(s.st_mode) != 0o600):
- raise RegistrationError('REGISTRATION_CONFIG_UNSAFE')
- def _read_maintenance_config():
- home = Path(os.environ.get('HERMES_HOME') or Path.home())
- path = home / 'plugins' / 'wecom-identity' / 'registration.json'
- _check_private_file(path)
- if path.stat().st_size > 4096:
- raise RegistrationError('REGISTRATION_CONFIG_INVALID')
- data = json.loads(path.read_text(encoding='utf-8'))
- if not isinstance(data, dict) or set(data) != {'version', 'agentProfileKey', 'botIdSha256'}:
- raise RegistrationError('REGISTRATION_CONFIG_INVALID')
- if data['version'] != 'ksf.current-employee-registration.v1':
- raise RegistrationError('REGISTRATION_CONFIG_INVALID')
- if not isinstance(data['agentProfileKey'], str) or not re.fullmatch(r'agent-account:[a-f0-9]{64}', data['agentProfileKey']):
- raise RegistrationError('REGISTRATION_CONFIG_INVALID')
- if not isinstance(data['botIdSha256'], str) or not re.fullmatch(r'[a-f0-9]{64}', data['botIdSha256']):
- raise RegistrationError('REGISTRATION_CONFIG_INVALID')
- return data
- def _current_identity():
- from plugins.platforms.wecom.identity_bridge import current_identity
- return current_identity() # No model args, reader override, or message-id override.
- def _receipt(result, agent_key):
- # Reconstruct the public receipt; do not echo worker stderr or raw account fields.
- if not isinstance(result, dict) or result.get('ok') is not True or result.get('agentProfileKey') != agent_key:
- raise RegistrationError('REGISTRATION_RECEIPT_INVALID')
- p = result.get('profile')
- if not isinstance(p, dict) or not re.fullmatch(r'participant-account:[a-f0-9]{64}', str(p.get('key', ''))):
- raise RegistrationError('REGISTRATION_RECEIPT_INVALID')
- if p.get('identityBasis') != 'channel_context' or not isinstance(p.get('name'), str) or not p['name'].strip():
- raise RegistrationError('REGISTRATION_RECEIPT_INVALID')
- if not isinstance(result.get('changed'), bool) or not isinstance(result.get('needsNameBinding'), bool):
- raise RegistrationError('REGISTRATION_RECEIPT_INVALID')
- iid = result.get('invocationId', '')
- if not isinstance(iid, str) or not re.fullmatch(r'[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}', iid):
- raise RegistrationError('REGISTRATION_RECEIPT_INVALID')
- return {
- 'ok': True, 'operation': 'current-employee-registered', 'invocationId': iid,
- 'changed': result['changed'], 'agentProfileKey': agent_key,
- 'profile': {'key': p['key'], 'name': p['name'], 'identityBasis': 'channel_context'},
- 'needsNameBinding': result['needsNameBinding'], 'currentMessageChecked': True,
- 'accountAuthenticated': False, 'displayNameAuthenticated': False,
- 'networkRequests': 0, 'databaseWrites': 0,
- }
- def _register_current_employee_result(args=None, **_kw):
- if args is not None and (not isinstance(args, dict) or args):
- return {'ok': False, 'error': 'NO_ARGUMENTS_ALLOWED'}
- try:
- checked = _current_identity()
- if not isinstance(checked, dict) or checked.get('ok') is not True:
- return {'ok': False, 'error': 'IDENTITY_CONTEXT_UNAVAILABLE'}
- identity = checked.get('identity')
- if not isinstance(identity, dict) or not isinstance(identity.get('botId'), str):
- raise RegistrationError('INVALID_CHANNEL_CONTEXT')
- config = _read_maintenance_config()
- if hashlib.sha256(identity['botId'].encode('utf-8')).hexdigest() != config['botIdSha256']:
- raise RegistrationError('REGISTRATION_INSTANCE_MISMATCH')
- node = shutil.which('node')
- if not node or not WORKER.is_file() or WORKER.is_symlink():
- raise RegistrationError('REGISTRATION_WORKER_UNAVAILABLE')
- request = {'version': config['version'], 'agentProfileKey': config['agentProfileKey'], 'identity': identity}
- proc = subprocess.run([node, str(WORKER)], input=json.dumps(request, ensure_ascii=False),
- capture_output=True, text=True, encoding='utf-8', timeout=20, check=False)
- if proc.returncode != 0:
- raise RegistrationError('REGISTRATION_WORKER_FAILED')
- if len(proc.stdout) > 16384:
- raise RegistrationError('REGISTRATION_RECEIPT_INVALID')
- return _receipt(json.loads(proc.stdout), config['agentProfileKey'])
- except RegistrationError as exc:
- return {'ok': False, 'error': str(exc)}
- except Exception:
- # Exceptions can contain config/worker output; no raw exception logging.
- return {'ok': False, 'error': 'IDENTITY_REGISTRATION_FAILED'}
- def register_current_employee(args=None, **_kw):
- """Hermes tool boundary accepts text; internal callers keep structured data."""
- return json.dumps(_register_current_employee_result(args, **_kw), ensure_ascii=False)
- def register(ctx):
- ctx.register_tool(TOOL_NAME, 'wecom', TOOL_SCHEMA, register_current_employee,
- description=TOOL_SCHEMA['description'], emoji='🪪')
|