Repository navigation
Expand file tree
/
Copy pathcheck_reference.py
More file actions
224 lines (214 loc) · 17.8 KB
/
Copy pathcheck_reference.py
File metadata and controls
224 lines (214 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
"""Executable local reference checks; no independent or live-agent evidence."""
import base64, copy, hashlib, io, json, sys, tempfile, zipfile
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
from reference.common import Invalid, Unsupported, decode, encode, load, pointer, relative
from reference.core import validate_document, history_state
from reference.continuation import inspect_session
from reference.context import predicate, resolve_configuration, evaluate_policy, reconstruct_request
from reference.workspace import restore, workspace_states
from reference.package import package_bytes, inspect_package, signature, verify_signature
from reference.redaction import audit
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from jsonschema.exceptions import ValidationError
ROOT=Path(__file__).resolve().parents[1];results=[]
def check(name,fn):
fn();results.append({'case':name,'passed':True})
def rejects(fn,expected):
try:fn()
except (Invalid,ValidationError) as exc:assert expected in str(exc),(expected,str(exc))
else:raise AssertionError('accepted invalid input: '+expected)
def equal(a,b):assert a==b,(a,b)
for file in sorted((ROOT/'examples').rglob('*.session.json')):
check('validate-'+file.stem,lambda f=file:validate_document(load(f)[0],f.parent))
def session_case(case):
d=load(ROOT/'examples'/case['fixture'])[0]
for edit in case['edits']:
target=d
for key in edit['path'][:-1]:target=target[int(key) if isinstance(target,list) else key]
key=int(edit['path'][-1]) if isinstance(target,list) else edit['path'][-1]
if edit.get('remove'):del target[key]
elif isinstance(target,list) and key==len(target):target.append(copy.deepcopy(edit['value']))
else:target[key]=copy.deepcopy(edit['value'])
if case.get('schema_error'):
try:validate_document(d,ROOT/'examples')
except ValidationError:pass
else:raise AssertionError('accepted structurally invalid task event')
elif 'error' in case:
rejects(lambda:validate_document(d,ROOT/'examples'),case['error'])
else:
validate_document(d,ROOT/'examples')
events={e['id']:e for e in d['events']}
if 'task_states' in case:
partial=next(c['status'] for c in d['coverage'] if c['scope']=='tasks')=='partial'
for branch in d['branches']:
if branch['id'] not in case['task_states']:continue
state=history_state([events[id] for id in branch['event_ids']],partial_tasks=partial)
actual={key:state[key] for key in ('task_dependencies','task_history_gaps')}
actual['tasks']={id:{k:task[k] for k in ('revision','status')} for id,task in state['tasks'].items()}
equal(actual,case['task_states'][branch['id']])
return
external_calls={b['id']:b['descriptor'] for b in d.get('external_bindings',[]) if b['kind']=='call'}
state=history_state([events[id] for id in d['branches'][0]['event_ids']],external_calls)
actual={'calls':{id:{k:call[k] for k in ('arguments','arguments_status')} for id,call in state['calls'].items()},'closed_calls':state['closed_calls']}
equal(actual,case['expected'])
for case in json.loads((ROOT/'tests/tool-call-progression-cases.json').read_text())['cases']:
check('call-progression-'+case['name'],lambda c=case:session_case(c))
for case in json.loads((ROOT/'tests/task-semantics-cases.json').read_text())['cases']:
check('task-semantics-'+case['name'],lambda c=case:session_case(c))
def preserved_partial_capture():
partial=load(ROOT/'examples/tool-call-partial.session.json')[0]
completed=load(ROOT/'examples/tool-call-completed.session.json')[0]
equal(partial['session']['id'],completed['session']['id'])
assert partial['capture']['id']!=completed['capture']['id']
for collection in ('events','resources','streams','contexts'):
later={x['id']:x for x in completed[collection]}
for record in partial[collection]:equal(record,later[record['id']])
validated=validate_document(completed,ROOT/'examples')
inputs=reconstruct_request(completed,validated,'context-completed')['inputs']
calls=[item for item in inputs if item['kind']=='tool_call']
equal(len(calls),1);equal(calls[0]['arguments'],{'document_id':'meeting-42'})
equal(calls[0]['source_events'],[{'event_id':'call-complete'}])
check('call-progression-retains-immutable-evidence',preserved_partial_capture)
folder=ROOT/'examples/continuation';doc,raw=load(folder/'another-computer.session.json');validated=validate_document(doc,folder)
check('strict-duplicate-key',lambda:rejects(lambda:decode(b'{"a":1,"a":2}'),'duplicate JSON key'))
check('strict-nonfinite',lambda:rejects(lambda:decode(b'{"a":NaN}'),'non-JSON numeric constant'))
check('bounded-depth',lambda:rejects(lambda:decode(('['*130+'0'+']'*130).encode()),'nesting limit'))
check('unknown-number-preservation',lambda:equal(decode(encode(decode(b'{"future":0.123456789012345678901234567890}'))),decode(b'{"future":0.123456789012345678901234567890}')))
check('pointer-escaped-keys',lambda:equal(pointer({'a/b':{'~':[2]}},'/a~1b/~0/0'),2))
check('pointer-invalid-escape',lambda:rejects(lambda:pointer({},'/a~2'),'invalid pointer escape'))
for path in ['../a','/tmp/a','a\\b','C:a','NUL','a/../b','a.']:
check('path-'+path,lambda p=path:rejects(lambda:relative(p),'path'))
check('request-typed-inputs',lambda:equal(reconstruct_request(doc,validated,doc['contexts'][0]['id'])['inputs'],doc['contexts'][0]['inputs']))
regressions=json.loads((ROOT/'tests/validation-regression-cases.json').read_text())
def request_resource_case(case):
d=copy.deepcopy(doc);tool=d['tools'][0]
if case.get('unselected'):
tool=copy.deepcopy(tool);tool['id']='unused-tool';d['tools'].append(tool)
tool['resource_ids']=case['tool_resources']
if case.get('lookalikes'):tool['input_schema'].update(resource_id='missing',resource_ids=['missing'])
if 'availability' in case:
resource={'id':'tool-asset','media_type':'text/plain','purpose':'input','availability':case['availability']}
resource['explanation']='Not captured.'
if case['availability']=='external':resource['locator']='https://example.invalid/tool-asset'
d['resources'].append(resource)
def project():return reconstruct_request(d,validate_document(d,folder),d['contexts'][0]['id'])
if 'error' in case:rejects(project,case['error'])
else:equal([r['id'] for r in project()['resources']],case['expected'])
for case in regressions['request_resources']:
check('request-'+case['name'],lambda c=case:request_resource_case(c))
def deletion_case(case):
d=copy.deepcopy(doc);w=d['continuation']['workspaces'][0]
w['entries']=[{**w['entries'][0],'path':p} for p in ['folder/public','folder/private','folder/secret/child']]
w['selection']['include']=['folder/**']
# A no-op intermediate delta exercises deletion through a base chain.
middle=copy.deepcopy(w);middle.update(id='middle',mode='delta',base_snapshot_id=w['id'],entries=[],deletions=[])
delta=copy.deepcopy(middle);delta.update(id='delta',base_snapshot_id='middle',deletions=[case['deletion']])
delta['selection']['exclude']=case['exclude'];d['continuation']['workspaces']+=[middle,delta]
d['continuation']['plans'][0]['workspace_ids']=['delta'];before=copy.deepcopy(d)
if 'error' in case:rejects(lambda:inspect_session(d,folder),case['error'])
else:
inspect_session(d,folder);states=workspace_states(d['continuation'])
equal(states['delta'],{});equal(states[w['id']],states['middle'])
equal(d,before)
for case in regressions['workspace_deletions']:
check('delta-'+case['name'],lambda c=case:deletion_case(c))
def destination_case(case):
d=copy.deepcopy(doc);w=d['continuation']['workspaces'][0]
w['entries']=[{'path':p,'kind':'directory','mode':493} if p==case.get('directory') else {**w['entries'][0],'path':p} for p in case['paths']]
with tempfile.TemporaryDirectory() as temp:
destination=Path(temp)/'restored'
def run():return restore(d,validated,w['id'],destination,case_sensitive=case['case_sensitive'],normalization=case['normalization'])
if 'error' in case:
rejects(run,case['error']);equal(list(Path(temp).iterdir()),[])
elif case['name'].startswith(('case-sensitive','unnormalized')):
# The host filesystem may not support these declared destination semantics.
from reference.workspace import validate_destination_paths
validate_destination_paths(workspace_states(d['continuation'])[w['id']],case_sensitive=case['case_sensitive'],normalization=case['normalization'])
else:run();assert (destination/'dir/child').is_file()
for case in regressions['destination_paths']:
check('restore-'+case['name'],lambda c=case:destination_case(c))
check('configuration-order',lambda:equal(resolve_configuration(doc,doc['configurations'][0]['id'],{'root_id':doc['continuation']['workspaces'][0]['root_id'],'relative_path':''})['instruction_ids'],doc['continuation']['configuration_bindings'][0]['effective_order']))
check('predicate-component-prefix',lambda:equal(predicate({'path_prefix':'src'},{'relative_path':'src-other/file'}),False))
check('predicate-combination',lambda:equal(predicate({'all':[{'event_kind_in':['tool_call']},{'not':{'root_is':'other'}}]},{'event_kind':'tool_call','root_id':'work'}),True))
check('predicate-unknown',lambda:rejects(lambda:predicate({'execute':'anything'},{}),'unknown predicate'))
policy=lambda effect,enforcing=True:{'id':effect,'type':'asif.policy/0.1','enforcing':enforcing,'definition':{'effect':effect,'tool_ids':['*'],'scope':{'kind':'global'},'activation':{'kind':'always'}}}
check('policy-deny-precedence',lambda:equal(evaluate_policy({'policies':[policy('allow'),policy('deny')]},{})['decision'],'deny'))
check('policy-default-ask',lambda:equal(evaluate_policy({'policies':[]},{})['decision'],'ask'))
def event(id,kind,data,**kw):return {'id':id,'kind':kind,'data':data,**kw}
request=event('q','decision_request',{'request_id':'q','options':[{'id':'yes','label':'Yes'}]})
answer=event('a','decision_resolution',{'request_id':'q','outcome':'allowed'})
change=event('b','decision_resolution',{'request_id':'q','outcome':'denied'})
check('contradictory-decisions',lambda:rejects(lambda:history_state([request,answer,change]),'contradictory decision'))
check('explicit-decision-amendment',lambda:equal(history_state([request,answer,{**change,'supersedes':{'event_id':'a'}}])['decisions']['q']['outcome'],'denied'))
task=event('t1','task_update',{'task_id':'task','revision':1,'status':'completed'})
reopen=event('t2','task_update',{'task_id':'task','revision':2,'previous_revision':1,'status':'pending'})
check('unexplained-task-reopen',lambda:rejects(lambda:history_state([task,reopen]),'reopen reason'))
check('explained-task-reopen',lambda:history_state([task,event('t2','task_update',{**reopen['data'],'reopen_reason':'New requirement'})]))
call=event('c','tool_call',{'call_id':'c'});res=event('r','tool_result',{'call_id':'c','result_index':0,'terminal':True,'outcome':'success'})
check('terminal-result-final',lambda:rejects(lambda:history_state([call,res,event('r2','tool_result',{**res['data'],'result_index':1})]),'after terminal'))
def source_mutation(fn,message):
d=copy.deepcopy(doc);fn(d);rejects(lambda:validate_document(d,folder),message)
check('missing-actor',lambda:source_mutation(lambda d:d['events'][0].update(actor_id='absent'),'missing actor'))
check('provenance-span',lambda:source_mutation(lambda d:d['events'][0]['provenance'].update(sources=[{'resource_id':d['resources'][0]['id'],'locator':{'syntax':'bytes','offset':999999,'length':1}}]),'source span outside'))
check('causal-self-cycle',lambda:source_mutation(lambda d:d['events'][0].update(causes=[{'event_id':d['events'][0]['id']}]),'branch causal order'))
def stream_fixture():
d=copy.deepcopy(doc);e=next(e for e in d['events'] if e['kind']=='tool_call');content=encode(e['data']['arguments']).decode()
r={'id':'stream-bytes','media_type':'application/json','purpose':'native','availability':'embedded','text':content,'bytes':len(content.encode()),'sha256':hashlib.sha256(content.encode()).hexdigest()};d['resources'].append(r)
for c in d['coverage']:
if c['scope']=='native':c.update(status='complete')
d['required_features'].append('asif.streams/0.1');d['streams']=[{'id':'stream-1','kind':'tool_arguments','event_id':e['id'],'call_id':e['data']['call_id'],'status':'complete','segments':[{'index':0,'resource_id':r['id'],'offset':0,'length':3,'terminal':False},{'index':1,'resource_id':r['id'],'offset':3,'length':r['bytes']-3,'terminal':True}]}];return d
check('stream-assembled-arguments',lambda:equal(validate_document(stream_fixture(),folder)['streams']['stream-1']['status'],'complete'))
def broken_stream():
d=stream_fixture();d['streams'][0]['segments'][1]['index']=2;rejects(lambda:validate_document(d,folder),'completeness mismatch')
check('stream-missing-fragment',broken_stream)
def external():
d=copy.deepcopy(doc);e=next(e for e in d['events'] if e['kind']=='tool_call');d['events'].remove(e)
for b in d['branches']:b['event_ids'].remove(e['id'])
for ev in d['events']:ev['causes']=[x for x in ev['causes'] if x['event_id']!=e['id']]
for c in d['contexts']:
for i in c['inputs']:i['source_events']=[x for x in i['source_events'] if x['event_id']!=e['id']]
d['required_features'].append('asif.external-bindings/0.1');d['external_bindings']=[{'kind':'call','id':e['data']['call_id'],'source':{'session_id':'earlier','capture_id':'earlier-capture','event_id':e['id']},'descriptor':{k:e['data'][k] for k in ['tool_id','arguments','arguments_status']}}]
return validate_document(d,folder)
check('external-call-binding',external)
with tempfile.TemporaryDirectory() as temp:
temp=Path(temp);w=doc['continuation']['workspaces'][0]
check('restore-selected-tree',lambda:restore(doc,validated,w['id'],temp/'restored'))
for entry in w['entries']:
if entry['kind']=='file':check('restored-bytes-'+entry['path'],lambda e=entry:equal((temp/'restored'/e['path']).read_bytes(),validated['resources'].bytes(e['resource_id'])))
check('restore-no-overwrite',lambda:rejects(lambda:restore(doc,validated,w['id'],temp/'restored'),'already exists'))
check('restore-rollback',lambda:rejects(lambda:restore(doc,validated,w['id'],temp/'failed',fail_after=1),'injected staging'))
check('rollback-cleaned',lambda:equal([x.name for x in temp.iterdir()],['restored']))
delta=copy.deepcopy(w);delta.update(id='delta',mode='delta',base_snapshot_id=w['id'],entries=[],deletions=[w['entries'][0]['path']])
check('delta-deletion',lambda:equal(w['entries'][0]['path'] in workspace_states({'workspaces':[w,delta]})['delta'],False))
bad=copy.deepcopy(delta);bad['deletions']=['missing'];check('delta-absent-deletion',lambda:rejects(lambda:workspace_states({'workspaces':[w,bad]}),'absent from base'))
collision=copy.deepcopy(doc);extra=copy.deepcopy(w['entries'][0]);extra['path']=extra['path'].upper();collision['continuation']['workspaces'][0]['entries'].append(extra)
check('restore-case-collision',lambda:rejects(lambda:restore(collision,validated,w['id'],temp/'collision',case_sensitive=False),'path collision'))
source=ROOT/'examples/image-and-document.session.json';packed=package_bytes(source)
check('deterministic-package',lambda:equal(packed,package_bytes(source)))
check('package-raw-document',lambda:equal(inspect_package(packed)['session.json'],source.read_bytes()))
key=Ed25519PrivateKey.generate();sig=signature(packed,key.private_bytes_raw());public=key.public_key().public_bytes_raw()
check('signature-verification',lambda:equal(verify_signature(packed,sig,public)['status'],'verified'))
check('signature-wrong-key',lambda:rejects(lambda:verify_signature(packed,sig,Ed25519PrivateKey.generate().public_key().public_bytes_raw()),'untrusted signing key'))
def changed_zip(name,content,compression=zipfile.ZIP_STORED):
files=inspect_package(packed);files[name]=content;out=io.BytesIO()
with zipfile.ZipFile(out,'w',compression=compression) as z:
for n,v in files.items():z.writestr(n,v)
return out.getvalue()
check('package-content-tamper',lambda:rejects(lambda:inspect_package(changed_zip('session.json',b'{}')),'inventory length mismatch'))
check('package-traversal',lambda:rejects(lambda:inspect_package(changed_zip('../outside',b'')),'unsafe path'))
check('package-unlisted-member',lambda:rejects(lambda:inspect_package(changed_zip('unlisted',b'')),'unlisted package member'))
check('package-compression-refused',lambda:rejects(lambda:inspect_package(changed_zip('session.json',source.read_bytes(),zipfile.ZIP_DEFLATED)),'unsupported package encoding'))
def redaction():
d=copy.deepcopy(doc);d['secret-demo']='needle';d['encoded-demo']=base64.b64encode(b'needle').decode();d['needle']={'needle':'needle'}
result=audit(encode(d),validated,['needle']);equal(result['status'],'matches_found');assert 'needle' not in json.dumps(result);assert any(x['location'].endswith('/base64') for x in result['findings'])
check('redaction-duplicates-base64-no-value-leak',redaction)
def capability_vector(vector):
from reference.capabilities import SNAPSHOT
raw=vector['utf8'].encode('utf-8')
equal(len(raw),vector['bytes']);equal(hashlib.sha256(raw).hexdigest(),vector['sha256'])
SNAPSHOT.validate(decode(raw))
for vector in json.loads((ROOT/'tests/capability-snapshot-vectors.json').read_text())['vectors']:
check('capability-hash-vector-'+vector['name'],lambda v=vector:capability_vector(v))
summary={'asif_version':'0.4','scope':'Local vendor-neutral reference implementation checks','checks':len(results),'passed':len(results),'independent_implementations':0,'real_runtime_tests':0,'results':results}
(ROOT/'tests/reference-results.json').write_text(json.dumps(summary,indent=2)+'\n');print(json.dumps({k:v for k,v in summary.items() if k!='results'},indent=2))