11import argparse
22import json
3+ import logging
34import sys
45import time
5- import logging
66from typing import Any
77
8- from intent_bus import IntentClient , WorkerRuntime , IntentBusError
8+ from intent_bus import IntentBusError , IntentClient , WorkerRuntime
9+ from intent_bus .exceptions import IntentBusLeaseLostError
10+
911
1012def log (level : str , event : str , message : str = '' , silent : bool = False ):
11- if silent and level .lower () not in ('error' , 'crit' ):
13+ if silent and level .lower () not in ('error' , 'crit' , 'critical' ):
1214 return
1315 ts = time .strftime ('%H:%M:%S' )
1416 print (f'[{ ts } ] { level .upper ():<5} | { event :<12} | { message } ' )
1517
18+
19+ def _redact_payload (payload : Any ) -> Any :
20+ """
21+ Mask sensitive fields to prevent accidental credential leaks in CI/CD logs.
22+
23+ Redacts common secret-like keys while avoiding overly broad matches
24+ such as 'monkey' or 'keypad'.
25+ """
26+ sensitive_keys = {
27+ 'secret' ,
28+ 'token' ,
29+ 'api_key' ,
30+ 'apikey' ,
31+ 'password' ,
32+ 'passwd' ,
33+ 'auth' ,
34+ 'credential' ,
35+ 'credentials' ,
36+ 'private_key' ,
37+ }
38+
39+ def is_sensitive_key (key : Any ) -> bool :
40+ k = str (key ).strip ().lower ()
41+ return (
42+ k in sensitive_keys
43+ or k .endswith ('_secret' )
44+ or k .endswith ('_token' )
45+ or k .endswith ('_key' )
46+ or k .endswith ('_password' )
47+ or k .endswith ('_passwd' )
48+ or k .endswith ('_auth' )
49+ or k .endswith ('_credential' )
50+ or k .endswith ('_credentials' )
51+ or k .endswith ('_private_key' )
52+ )
53+
54+ if isinstance (payload , dict ):
55+ redacted = {}
56+ for k , v in payload .items ():
57+ if is_sensitive_key (k ):
58+ redacted [k ] = '********'
59+ else :
60+ redacted [k ] = _redact_payload (v )
61+ return redacted
62+
63+ if isinstance (payload , list ):
64+ return [_redact_payload (item ) for item in payload ]
65+
66+ return payload
67+
68+
1669def main ():
1770 parser = argparse .ArgumentParser (
18- prog = 'intent-bus' ,
71+ prog = 'intent-bus' ,
1972 formatter_class = argparse .RawDescriptionHelpFormatter ,
2073 description = (
21- "Intent Bus CLI | Protocol v2.0 \n "
74+ "Intent Bus CLI | Protocol v2.1 \n "
2275 "A lightweight, distributed job bus toolkit."
2376 ),
2477 epilog = (
@@ -28,44 +81,49 @@ def main():
2881 "\n "
2982 "environment variables:\n "
3083 " INTENT_API_KEY Your API key (or place it in ~/.apikey)\n "
31- )
84+ ),
3285 )
33-
86+
3487 base_parser = argparse .ArgumentParser (add_help = False )
35- base_parser .add_argument ('-s' , '--silent' , action = 'store_true' , help = 'Suppress non-critical logs, payloads, and network warnings' )
88+ base_parser .add_argument (
89+ '-s' , '--silent' ,
90+ action = 'store_true' ,
91+ help = 'Suppress non-critical logs, payloads, and network warnings' ,
92+ )
3693
3794 subparsers = parser .add_subparsers (dest = 'command' , title = 'commands' )
3895
3996 listen_parser = subparsers .add_parser (
40- 'listen' ,
97+ 'listen' ,
4198 parents = [base_parser ],
4299 help = 'Start a worker node loop to claim and process intents' ,
43100 formatter_class = argparse .RawDescriptionHelpFormatter ,
44101 description = "Start a continuous worker polling loop. Honors server-directed backoffs." ,
45- epilog = "example:\n intent-bus listen encode_video -n media -c ffmpeg,gpu --interval 2.5"
102+ epilog = "example:\n intent-bus listen encode_video -n media -c ffmpeg,gpu --interval 2.5" ,
46103 )
47104 listen_parser .add_argument ('goal' , help = 'The specific intent goal to claim (e.g., process_image)' )
48105 listen_parser .add_argument ('-n' , '--namespace' , default = 'default' , help = 'Routing namespace (default: default)' )
49106 listen_parser .add_argument ('-w' , '--worker-id' , help = 'Explicit worker identity for tracking' )
50107 listen_parser .add_argument ('-c' , '--capabilities' , help = 'Comma-separated worker capabilities (e.g., gpu,ocr)' )
51108 listen_parser .add_argument ('--once' , action = 'store_true' , help = 'Process exactly one job and exit immediately' )
52109 listen_parser .add_argument ('--interval' , type = float , default = 5.0 , help = 'Base polling interval in seconds (default: 5.0)' )
110+ listen_parser .add_argument ('--show-payload' , action = 'store_true' , help = 'Print full, unredacted JSON payloads (WARNING: may leak secrets)' )
53111
54112 pub_parser = subparsers .add_parser (
55- 'publish' ,
113+ 'publish' ,
56114 parents = [base_parser ],
57115 help = 'Publish a new intent to the bus' ,
58116 formatter_class = argparse .RawDescriptionHelpFormatter ,
59117 description = "Push a new JSON payload to the bus for workers to claim." ,
60- epilog = "example:\n intent-bus publish send_email '{\" to\" : \" user@ext.com\" }' -n comms"
118+ epilog = "example:\n intent-bus publish send_email '{\" to\" : \" user@ext.com\" }' -n comms" ,
61119 )
62120 pub_parser .add_argument ('goal' , help = 'The goal/target for this intent' )
63121 pub_parser .add_argument ('data' , help = 'Strict JSON payload string (must use double quotes for keys)' )
64122 pub_parser .add_argument ('-n' , '--namespace' , default = 'default' , help = 'Routing namespace (default: default)' )
65123 pub_parser .add_argument ('-p' , '--public' , action = 'store_true' , help = 'Make intent visible across all namespaces' )
66124
67125 args = parser .parse_args ()
68-
126+
69127 if not args .command :
70128 parser .print_help ()
71129 sys .exit (0 )
@@ -75,9 +133,9 @@ def main():
75133 logging .basicConfig (
76134 level = logging .ERROR if is_silent else logging .INFO ,
77135 format = '[%(asctime)s] %(levelname)-5s | %(name)s | %(message)s' ,
78- datefmt = '%H:%M:%S'
136+ datefmt = '%H:%M:%S' ,
79137 )
80-
138+
81139 if is_silent :
82140 logging .disable (logging .WARNING )
83141
@@ -87,63 +145,87 @@ def main():
87145 log ('crit' , 'auth_fail' , str (e ), is_silent )
88146 sys .exit (1 )
89147
90- caps = ([c .strip () for c in args .capabilities .split (',' ) if c .strip ()] if args .capabilities else None )
148+ caps = (
149+ [c .strip () for c in args .capabilities .split (',' ) if c .strip ()]
150+ if getattr (args , 'capabilities' , None )
151+ else None
152+ )
91153
92154 if args .command == 'listen' :
93155 def process_job (payload : Any ) -> dict :
94156 if not is_silent :
95157 border = '─' * 40
96158 print (f'\n { border } ' )
97- print (json .dumps (payload , indent = 2 , ensure_ascii = False , default = str ))
159+ display_payload = payload if getattr (args , 'show_payload' , False ) else _redact_payload (payload )
160+ print (json .dumps (display_payload , indent = 2 , ensure_ascii = False , default = str ))
98161 print (f'{ border } \n ' )
99162 return {'status' : 'processed_by_cli' }
100163
101164 if args .once :
102165 job = client .claim (
103- goal = args .goal ,
104- namespace = args .namespace ,
105- worker_id = args .worker_id ,
106- capabilities = caps
166+ goal = args .goal ,
167+ namespace = args .namespace ,
168+ worker_id = args .worker_id ,
169+ capabilities = caps ,
107170 )
108-
171+
109172 if job :
110173 log ('info' , 'claiming' , f'ID: { job .id } ' , is_silent )
111174 try :
112175 res = process_job (job .payload )
113- client .fulfill (job .id , result = res )
176+ client .fulfill (
177+ intent_id = job .id ,
178+ claim_token = job .claim_token ,
179+ result = res ,
180+ )
114181 log ('info' , 'fulfilled' , f'ID: { job .id } ' , is_silent )
182+
183+ except IntentBusLeaseLostError as e :
184+ log ('error' , 'lease_lost' , str (e ), is_silent )
185+
115186 except Exception as e :
116187 log ('error' , 'handler_err' , str (e ), is_silent )
117- client .fail (job .id , error = str (e ))
188+ try :
189+ client .fail (
190+ intent_id = job .id ,
191+ claim_token = job .claim_token ,
192+ error = str (e ),
193+ )
194+ except IntentBusLeaseLostError as fail_err :
195+ log ('error' , 'lease_lost' , str (fail_err ), is_silent )
196+ except Exception as fail_err :
197+ log ('error' , 'fail_err' , str (fail_err ), is_silent )
198+
118199 else :
119200 log ('info' , 'idle' , f'No jobs found for { args .goal } .' , is_silent )
201+
120202 return
121203
122204 runtime = WorkerRuntime (
123205 client = client ,
124206 worker_id = args .worker_id ,
125207 capabilities = caps ,
126208 poll_interval = args .interval ,
127- close_client = True
209+ close_client = True ,
128210 )
129211
130212 log ('info' , 'worker_up' , f'Target: { args .namespace } /{ args .goal } ' , is_silent )
131213 runtime .listen (
132214 goal = args .goal ,
133215 handler = process_job ,
134- namespace = args .namespace
216+ namespace = args .namespace ,
135217 )
136218
137219 elif args .command == 'publish' :
138220 try :
139221 payload = json .loads (args .data )
140222 res = client .publish (
141- goal = args .goal ,
142- payload = payload ,
143- namespace = args .namespace ,
144- visibility = 'public' if args .public else 'private'
223+ goal = args .goal ,
224+ payload = payload ,
225+ namespace = args .namespace ,
226+ visibility = 'public' if args .public else 'private' ,
145227 )
146- if res :
228+ if res :
147229 log ('info' , 'pub_done' , f'ID: { res .id } ' , is_silent )
148230 else :
149231 log ('error' , 'pub_fail' , 'Server rejected the intent.' , is_silent )
@@ -152,5 +234,6 @@ def process_job(payload: Any) -> dict:
152234 except IntentBusError as e :
153235 log ('error' , 'pub_err' , str (e ), is_silent )
154236
237+
155238if __name__ == '__main__' :
156239 main ()
0 commit comments