#!/usr/bin/env python3 """Direct HTTPS task client. No browser/MCP, shell execution, or automatic retry.""" import argparse,json,os,stat,sys,uuid,ssl,hashlib,tempfile from pathlib import Path from urllib.request import Request,build_opener,HTTPSHandler,HTTPRedirectHandler from urllib.error import HTTPError,URLError BASE='https://hq.wfcloud.app/api/v1/records' class SafeError(Exception):pass class NoRedirect(HTTPRedirectHandler): def redirect_request(self,*a,**k):return None def load_token(): # Environment contains only a path, never the token. No credential discovery. path=os.environ.get('HQ_API_KEY_FILE') if not path:raise SafeError('Configure HQ_API_KEY_FILE to an owner-only secret file supplied by your authorized task environment.') p=Path(path) try: fd=os.open(p,os.O_RDONLY|getattr(os,'O_NOFOLLOW',0));s=os.fstat(fd) if not stat.S_ISREG(s.st_mode) or s.st_mode&0o077 or s.st_uid!=os.geteuid():os.close(fd);raise SafeError('Secret file must be regular, owned by this user, and mode0600 or0400.') with os.fdopen(fd,'rb') as f:raw=f.read(512).strip() except OSError:raise SafeError('Configured secret file could not be read securely.') try:token=raw.decode('ascii') except UnicodeDecodeError:raise SafeError('Secret file is not a valid HQ API key.') if not token.startswith('hq_key_') or any(c.isspace() for c in token) or not 35<=len(token)<=200:raise SafeError('Secret file is not a valid HQ API key.') return token def send(action,token,payload=None,record_id=None,request_id=None,opener=None): if action not in ['connect','list','get','create','edit','inbox-list','inbox-get','inbox-create','inbox-reply']:raise SafeError('Unsupported action') if action in ['get','edit','inbox-get','inbox-reply']: try: if str(uuid.UUID(record_id))!=record_id:raise ValueError() except (ValueError,TypeError):raise SafeError('A valid record UUID is required.') root=BASE.removesuffix('/records') if action.startswith('inbox-'): url=root+'/inbox/threads'+('/'+record_id if record_id else '')+('/messages' if action=='inbox-reply' else '') else:url=root+'/connect' if action=='connect' else BASE+('/'+record_id if record_id else '') method='GET' if action in ['connect','list','get','inbox-list','inbox-get'] else 'POST' if action in ['create','inbox-create','inbox-reply'] else 'PUT' headers={'Accept':'application/json','Authorization':'Bearer '+token} if method!='GET': request_id=request_id or str(uuid.uuid4());headers.update({'Content-Type':'application/json','Idempotency-Key':request_id}) req=Request(url,data=json.dumps(payload).encode() if method!='GET' else None,method=method,headers=headers) client=opener or build_opener(NoRedirect,HTTPSHandler(context=ssl.create_default_context())) try: with client.open(req,timeout=20) as r:result=json.load(r);briefing_version=r.headers.get('X-HQ-Workspace-Briefing') except HTTPError as e: code=e.code;e.close() return {'ok':False,'http_status':code,'request_id':request_id,'uncertain':method!='GET' and code>=500,'next':'For 5xx writes, outcome may be uncertain: retry only the identical request_id. 401: sign in to HQ and check key expiry/revocation. 403: check scopes/ownership. 409: refresh record or reuse the key only for the identical request. No automatic retry performed.'} except (URLError,TimeoutError,OSError,ValueError): return {'ok':False,'request_id':request_id,'uncertain':method!='GET','next':'Network/result uncertain. Retry only the identical request with this same request_id; never invent a new key for that retry.'} return {'ok':True,'request_id':request_id,'briefing_version':briefing_version,'result':result} def cache_path(token): return Path(os.environ.get('HQ_BRIEFING_CACHE_DIR',str(Path.home()/'.cache'/'office-hq')))/('briefing-'+hashlib.sha256(token.encode()).hexdigest()[:16]+'.json') def cached_version(path): try: s=path.lstat() if not stat.S_ISREG(s.st_mode) or s.st_uid!=os.geteuid() or s.st_mode&0o077:return None return json.loads(path.read_text()).get('version') except (OSError,ValueError,TypeError):return None def remember_version(path,version): if not version:return try: path.parent.mkdir(mode=0o700,parents=True,exist_ok=True) if path.parent.stat().st_uid!=os.geteuid():return fd,name=tempfile.mkstemp(prefix='.briefing-',dir=path.parent) try: with os.fdopen(fd,'w') as f:json.dump({'version':version},f) os.chmod(name,0o600);os.replace(name,path) finally: if os.path.exists(name):os.unlink(name) except OSError:pass def run_connected(action,token,payload=None,record_id=None,request_id=None,transport=send): path=cache_path(token);seen=cached_version(path);bootstrap=None if action=='connect' or not seen: bootstrap=transport('connect',token) if not bootstrap['ok']:return bootstrap version=bootstrap['result']['briefing']['version'];remember_version(path,version) if action=='connect':return bootstrap result=transport(action,token,payload,record_id,request_id) if result.get('ok') and result.get('briefing_version') and result['briefing_version']!=(bootstrap['result']['briefing']['version'] if bootstrap else seen): latest=transport('connect',token) if latest.get('ok'):bootstrap=latest;remember_version(path,latest['result']['briefing']['version']) if bootstrap:result['workspace_connection']=bootstrap['result'] return result def main(): p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=['connect','list','get','create','edit','inbox-list','inbox-get','inbox-create','inbox-reply']);p.add_argument('--record');p.add_argument('--request-id');args=p.parse_args() payload=None if args.action in ['create','edit','inbox-create','inbox-reply']: try:payload=json.loads(sys.stdin.read(200001)) except ValueError:raise SafeError('Provide a JSON draft on stdin.') if not isinstance(payload,dict):raise SafeError('Provide one JSON object.') result=run_connected(args.action,load_token(),payload,args.record,args.request_id) print(json.dumps(result,ensure_ascii=False));return 0 if result['ok'] else 1 if __name__=='__main__': try:sys.exit(main()) except SafeError as e:print(json.dumps({'ok':False,'error':str(e)}));sys.exit(1) except Exception:print(json.dumps({'ok':False,'error':'Task client failed; sensitive details withheld.'}));sys.exit(1)