Merge pull request 'Archive the completed project-member-runtime task card' (#613) from feature/card-close-project-member-runtime-20261018 into main
This commit was merged in pull request #613.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"authentication": "temporary API credentials; existing passwords unchanged; password login NOT_RUN",
|
||||
"hostProjectId": "prj_8d20020e5b79",
|
||||
"canonicalProjectId": "p-aea70b2559c6",
|
||||
"accounts": {
|
||||
"weichen": {
|
||||
"conversationCreatedByUI": "cnv_a0bab630d4b5",
|
||||
"projectVisible": true,
|
||||
"otherAccountConversationAbsent": true,
|
||||
"refreshPassed": true,
|
||||
"ownFileVisible": true,
|
||||
"otherAccountFileAbsent": true
|
||||
},
|
||||
"test": {
|
||||
"conversationCreatedByUI": "cnv_36fecdb2d986",
|
||||
"projectVisible": true,
|
||||
"otherAccountConversationAbsent": true,
|
||||
"refreshPassed": true,
|
||||
"ownFileVisible": true,
|
||||
"otherAccountFileAbsent": true
|
||||
}
|
||||
},
|
||||
"pageerrorCount": 0,
|
||||
"accountSwitchThroughLogout": true,
|
||||
"driverSHA256": "d8b6b344a29c8914367e9530e74ebdf027563859efa3809110b82335d97f9f3b"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"hostProjectId": "prj_8d20020e5b79",
|
||||
"canonicalProjectId": "p-aea70b2559c6",
|
||||
"account_visibility_and_private_creation": "PASS",
|
||||
"private_peer_status": {
|
||||
"weichen": {
|
||||
"other_conversation": 404,
|
||||
"other_original": 404
|
||||
},
|
||||
"test": {
|
||||
"other_conversation": 404,
|
||||
"other_original": 404
|
||||
}
|
||||
},
|
||||
"cross_project_status": {
|
||||
"conversation_list": 404,
|
||||
"conversation_create": 404,
|
||||
"artifacts": 404,
|
||||
"domain_summary": 400
|
||||
},
|
||||
"global_duplicate_name_status": 409,
|
||||
"preserved_business_rows": {
|
||||
"agent-system.db:users": {
|
||||
"old_preserved": 12,
|
||||
"current": 12
|
||||
},
|
||||
"agent-system.db:projects": {
|
||||
"old_preserved": 3,
|
||||
"current": 3
|
||||
},
|
||||
"agent-system.db:conversations": {
|
||||
"old_preserved": 7,
|
||||
"current": 9
|
||||
},
|
||||
"agent-system.db:messages": {
|
||||
"old_preserved": 35,
|
||||
"current": 35
|
||||
},
|
||||
"agent-system.db:artifacts": {
|
||||
"old_preserved": 14,
|
||||
"current": 16
|
||||
},
|
||||
"agent-system.db:agent_system_identity": {
|
||||
"old_preserved": 1,
|
||||
"current": 1
|
||||
},
|
||||
"rdm.db:rdm_projects": {
|
||||
"old_preserved": 1,
|
||||
"current": 2
|
||||
},
|
||||
"rdm.db:rdm_host_project_map": {
|
||||
"old_preserved": 1,
|
||||
"current": 2
|
||||
},
|
||||
"rdm.db:rdm_members": {
|
||||
"old_preserved": 6,
|
||||
"current": 8
|
||||
},
|
||||
"rdm.db:rdm_admins": {
|
||||
"old_preserved": 1,
|
||||
"current": 1
|
||||
}
|
||||
},
|
||||
"original_artifact_bytes_preserved": 14,
|
||||
"fixtures": {
|
||||
"conversations": {
|
||||
"weichen": "cnv_7c6d2eb980cc",
|
||||
"test": "cnv_dbae0c998eec"
|
||||
},
|
||||
"artifacts": {
|
||||
"weichen": {
|
||||
"id": "art_11ea0ec72767",
|
||||
"sha256": "816a6cbb70fcc550b860de78cf494f9d3bfd8f77a32c48d53a5dfd83359c1dfb"
|
||||
},
|
||||
"test": {
|
||||
"id": "art_922d016bf0a0",
|
||||
"sha256": "0d894a02ee84e7ba8122f06bf3a3f4d2c0d4c2ba0bab2575d8db285c9d158a3a"
|
||||
}
|
||||
}
|
||||
},
|
||||
"password_login": "NOT_RUN; user selected temporary API credentials"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import hashlib,json,secrets,sqlite3
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import client,prepare
|
||||
W=client.W;HOST=prepare.HOST
|
||||
adopt=json.loads((W/'adoption-result.json').read_text());PID=adopt['canonicalProjectId']
|
||||
state_path=W/'http-fixtures.json';state=json.loads(state_path.read_text()) if state_path.exists() else {'conversations':{},'artifacts':{}}
|
||||
def state_save():client.save('http-fixtures.json',state)
|
||||
def ok(owner,path,method='GET',data=None,raw=False,headers=None):
|
||||
code,out=client.request(owner,path,method,data,raw,headers);assert code==200,f'{owner} {method} {path} status={code}';return out
|
||||
def deny(owner,path,method='GET',data=None):
|
||||
code,_=client.request(owner,path,method,data);assert code in (403,404),f'{owner} {path} not denied ({code})';return code
|
||||
for owner in ['weichen','test']:
|
||||
me=ok(owner,'/api/me');assert me['owner']==owner
|
||||
projects=ok(owner,'/api/projects');p=next(x for x in projects if x['id']==HOST)
|
||||
assert p['settings']['rdm.projectId']==PID and p['settings']['rdm.state']=='ready'
|
||||
if owner not in state['conversations']:
|
||||
conv=ok(owner,'/api/projects/'+HOST+'/conversations','POST',{'title':'成员协作复测-'+owner+'-20261013'})
|
||||
assert conv['owner']==owner and conv['projectId']==HOST
|
||||
state['conversations'][owner]=conv['id'];state_save()
|
||||
own_id=state['conversations'][owner]
|
||||
assert ok(owner,'/api/conversations/'+own_id)['conversation']['owner']==owner
|
||||
if owner not in state['artifacts']:
|
||||
marker=('PROJECT_MEMBER_RUNTIME_PRIVATE_'+owner+'_20261013\n').encode()
|
||||
boundary='PMR'+secrets.token_hex(16)
|
||||
raw=(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="成员协作复测-{owner}-私有.txt"\r\nContent-Type: text/plain\r\n\r\n').encode()+marker+(f'\r\n--{boundary}--\r\n').encode()
|
||||
files=ok(owner,'/api/projects/'+HOST+'/uploads','POST',raw,headers={'Content-Type':'multipart/form-data; boundary='+boundary})
|
||||
state['artifacts'][owner]={'id':files[0]['artifactId'],'sha256':hashlib.sha256(marker).hexdigest()};state_save()
|
||||
a=state['artifacts'][owner]
|
||||
assert hashlib.sha256(ok(owner,'/api/artifacts/'+a['id']+'/content',raw=True)).hexdigest()==a['sha256']
|
||||
with prepare.db(prepare.DATA/'agent-system.db') as c:
|
||||
row=c.execute('select owner,project_id from artifacts where id=?',(a['id'],)).fetchone();assert tuple(row)==(owner,HOST)
|
||||
private_status={}
|
||||
for viewer,other in [('weichen','test'),('test','weichen')]:
|
||||
private_status[viewer]={'other_conversation':deny(viewer,'/api/conversations/'+state['conversations'][other]),'other_original':deny(viewer,'/api/artifacts/'+state['artifacts'][other]['id']+'/content')}
|
||||
assert state['conversations'][other] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/conversations')]
|
||||
assert state['artifacts'][other]['id'] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/artifacts')]
|
||||
foreign='prj_b32c62943fa3';foreign_pid='p-39dd859c3c99'
|
||||
assert foreign not in [x['id'] for x in ok('test','/api/projects')]
|
||||
foreign_status={'conversation_list':deny('test','/api/projects/'+foreign+'/conversations'),'conversation_create':deny('test','/api/projects/'+foreign+'/conversations','POST',{'title':'cross-project denial probe'}),'artifacts':deny('test','/api/projects/'+foreign+'/artifacts')}
|
||||
code,denied=client.request('test','/api/commands/rdm.inspect.summary','POST',{'params':{'projectId':foreign_pid},'entrance':'workspace'})
|
||||
assert code in (400,403,404) and denied.get('receipt',{}).get('state')=='rejected';foreign_status['domain_summary']=code
|
||||
code,_=client.request('test','/api/projects','POST',{'name':'genui-e2e'});assert code==409,'global duplicate name was not rejected';duplicate_status=code
|
||||
before=json.loads((W/'baseline-private.json').read_text());current=prepare.logical(prepare.DATA)
|
||||
backup=Path(json.loads((W/'backup-location.json').read_text())['path'])
|
||||
with prepare.db(backup/'dev/data/rdm.db') as c:
|
||||
moved=Counter(prepare.digest(x) for x in c.execute('select * from rdm_members where project_id=?',(HOST,)))
|
||||
preserved={}
|
||||
for table,b in before['logical'].items():
|
||||
expected=Counter(b['rows'])
|
||||
if table=='rdm.db:rdm_members':expected-=moved
|
||||
assert not expected-Counter(current[table]['rows']), 'old business rows changed: '+table
|
||||
preserved[table]={'old_preserved':sum(expected.values()),'current':len(current[table]['rows'])}
|
||||
for a in before['artifacts']:assert prepare.sha(Path(a['content_path']))==a['sha256']
|
||||
result={'source':'SELF','hostProjectId':HOST,'canonicalProjectId':PID,'account_visibility_and_private_creation':'PASS','private_peer_status':private_status,'cross_project_status':foreign_status,'global_duplicate_name_status':duplicate_status,'preserved_business_rows':preserved,'original_artifact_bytes_preserved':len(before['artifacts']),'fixtures':state,'password_login':'NOT_RUN; user selected temporary API credentials'}
|
||||
client.save('acceptance-http.json',result);print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,56 @@
|
||||
import hashlib,json,secrets,sqlite3
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import client,prepare
|
||||
W=client.W;HOST=prepare.HOST
|
||||
adopt=json.loads((W/'adoption-result.json').read_text());PID=adopt['canonicalProjectId']
|
||||
state_path=W/'http-fixtures.json';state=json.loads(state_path.read_text()) if state_path.exists() else {'conversations':{},'artifacts':{}}
|
||||
def state_save():client.save('http-fixtures.json',state)
|
||||
def ok(owner,path,method='GET',data=None,raw=False,headers=None):
|
||||
code,out=client.request(owner,path,method,data,raw,headers);assert code==200,f'{owner} {method} {path} status={code}';return out
|
||||
def deny(owner,path,method='GET',data=None):
|
||||
code,_=client.request(owner,path,method,data);assert code in (403,404),f'{owner} {path} not denied ({code})';return code
|
||||
for owner in ['weichen','test']:
|
||||
me=ok(owner,'/api/me');assert me['owner']==owner
|
||||
projects=ok(owner,'/api/projects');p=next(x for x in projects if x['id']==HOST)
|
||||
assert p['settings']['rdm.projectId']==PID and p['settings']['rdm.state']=='ready'
|
||||
if owner not in state['conversations']:
|
||||
conv=ok(owner,'/api/projects/'+HOST+'/conversations','POST',{'title':'成员协作复测-'+owner+'-20261013'})
|
||||
assert conv['owner']==owner and conv['projectId']==HOST
|
||||
state['conversations'][owner]=conv['id'];state_save()
|
||||
own_id=state['conversations'][owner]
|
||||
assert ok(owner,'/api/conversations/'+own_id)['conversation']['owner']==owner
|
||||
if owner not in state['artifacts']:
|
||||
marker=('PROJECT_MEMBER_RUNTIME_PRIVATE_'+owner+'_20261013\n').encode()
|
||||
boundary='PMR'+secrets.token_hex(16)
|
||||
raw=(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="成员协作复测-{owner}-私有.txt"\r\nContent-Type: text/plain\r\n\r\n').encode()+marker+(f'\r\n--{boundary}--\r\n').encode()
|
||||
files=ok(owner,'/api/projects/'+HOST+'/uploads','POST',raw,headers={'Content-Type':'multipart/form-data; boundary='+boundary})
|
||||
state['artifacts'][owner]={'id':files['artifactId'],'sha256':hashlib.sha256(marker).hexdigest()};state_save()
|
||||
a=state['artifacts'][owner]
|
||||
assert hashlib.sha256(ok(owner,'/api/artifacts/'+a['id']+'/content',raw=True)).hexdigest()==a['sha256']
|
||||
with prepare.db(prepare.DATA/'agent-system.db') as c:
|
||||
row=c.execute('select owner,project_id from artifacts where id=?',(a['id'],)).fetchone();assert tuple(row)==(owner,HOST)
|
||||
private_status={}
|
||||
for viewer,other in [('weichen','test'),('test','weichen')]:
|
||||
private_status[viewer]={'other_conversation':deny(viewer,'/api/conversations/'+state['conversations'][other]),'other_original':deny(viewer,'/api/artifacts/'+state['artifacts'][other]['id']+'/content')}
|
||||
assert state['conversations'][other] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/conversations')]
|
||||
assert state['artifacts'][other]['id'] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/artifacts')]
|
||||
foreign='prj_b32c62943fa3';foreign_pid='p-39dd859c3c99'
|
||||
assert foreign not in [x['id'] for x in ok('test','/api/projects')]
|
||||
foreign_status={'conversation_list':deny('test','/api/projects/'+foreign+'/conversations'),'conversation_create':deny('test','/api/projects/'+foreign+'/conversations','POST',{'title':'cross-project denial probe'}),'artifacts':deny('test','/api/projects/'+foreign+'/artifacts')}
|
||||
code,denied=client.request('test','/api/commands/rdm.inspect.summary','POST',{'params':{'projectId':foreign_pid},'entrance':'workspace'})
|
||||
assert code in (400,403,404) and denied.get('receipt',{}).get('state')=='rejected';foreign_status['domain_summary']=code
|
||||
code,_=client.request('test','/api/projects','POST',{'name':'genui-e2e'});assert code==409,'global duplicate name was not rejected';duplicate_status=code
|
||||
before=json.loads((W/'baseline-private.json').read_text());current=prepare.logical(prepare.DATA)
|
||||
backup=Path(json.loads((W/'backup-location.json').read_text())['path'])
|
||||
with prepare.db(backup/'dev/data/rdm.db') as c:
|
||||
moved=Counter(prepare.digest(x) for x in c.execute('select * from rdm_members where project_id=?',(HOST,)))
|
||||
preserved={}
|
||||
for table,b in before['logical'].items():
|
||||
expected=Counter(b['rows'])
|
||||
if table=='rdm.db:rdm_members':expected-=moved
|
||||
assert not expected-Counter(current[table]['rows']), 'old business rows changed: '+table
|
||||
preserved[table]={'old_preserved':sum(expected.values()),'current':len(current[table]['rows'])}
|
||||
for a in before['artifacts']:assert prepare.sha(Path(a['content_path']))==a['sha256']
|
||||
result={'source':'SELF','hostProjectId':HOST,'canonicalProjectId':PID,'account_visibility_and_private_creation':'PASS','private_peer_status':private_status,'cross_project_status':foreign_status,'global_duplicate_name_status':duplicate_status,'preserved_business_rows':preserved,'original_artifact_bytes_preserved':len(before['artifacts']),'fixtures':state,'password_login':'NOT_RUN; user selected temporary API credentials'}
|
||||
client.save('acceptance-http.json',result);print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,57 @@
|
||||
import hashlib,json,secrets,sqlite3
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import client,prepare
|
||||
W=client.W;HOST=prepare.HOST
|
||||
adopt=json.loads((W/'adoption-result.json').read_text());PID=adopt['canonicalProjectId']
|
||||
state_path=W/'http-fixtures.json';state=json.loads(state_path.read_text()) if state_path.exists() else {'conversations':{},'artifacts':{}}
|
||||
def state_save():client.save('http-fixtures.json',state)
|
||||
def ok(owner,path,method='GET',data=None,raw=False,headers=None):
|
||||
code,out=client.request(owner,path,method,data,raw,headers);assert code==200,f'{owner} {method} {path} status={code}';return out
|
||||
def deny(owner,path,method='GET',data=None):
|
||||
code,_=client.request(owner,path,method,data);assert code in (403,404),f'{owner} {path} not denied ({code})';return code
|
||||
for owner in ['weichen','test']:
|
||||
me=ok(owner,'/api/me');assert me['owner']==owner
|
||||
projects=ok(owner,'/api/projects');p=next(x for x in projects if x['id']==HOST)
|
||||
assert p['settings']['rdm.projectId']==PID and p['settings']['rdm.state']=='ready'
|
||||
if owner not in state['conversations']:
|
||||
conv=ok(owner,'/api/projects/'+HOST+'/conversations','POST',{'title':'成员协作复测-'+owner+'-20261013'})
|
||||
assert conv['owner']==owner and conv['projectId']==HOST
|
||||
state['conversations'][owner]=conv['id'];state_save()
|
||||
own_id=state['conversations'][owner]
|
||||
assert ok(owner,'/api/conversations/'+own_id)['conversation']['owner']==owner
|
||||
if owner not in state['artifacts']:
|
||||
marker=('PROJECT_MEMBER_RUNTIME_PRIVATE_'+owner+'_20261013\n').encode()
|
||||
boundary='PMR'+secrets.token_hex(16)
|
||||
raw=(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="成员协作复测-{owner}-私有.txt"\r\nContent-Type: text/plain\r\n\r\n').encode()+marker+(f'\r\n--{boundary}--\r\n').encode()
|
||||
files=ok(owner,'/api/projects/'+HOST+'/uploads','POST',raw,headers={'Content-Type':'multipart/form-data; boundary='+boundary})
|
||||
state['artifacts'][owner]={'id':files['artifactId'],'sha256':hashlib.sha256(marker).hexdigest()};state_save()
|
||||
a=state['artifacts'][owner]
|
||||
assert hashlib.sha256(ok(owner,'/api/artifacts/'+a['id']+'/content',raw=True)).hexdigest()==a['sha256']
|
||||
with prepare.db(prepare.DATA/'agent-system.db') as c:
|
||||
row=c.execute('select owner,project_id from artifacts where id=?',(a['id'],)).fetchone();assert tuple(row)==(owner,HOST)
|
||||
private_status={}
|
||||
for viewer,other in [('weichen','test'),('test','weichen')]:
|
||||
private_status[viewer]={'other_conversation':deny(viewer,'/api/conversations/'+state['conversations'][other]),'other_original':deny(viewer,'/api/artifacts/'+state['artifacts'][other]['id']+'/content')}
|
||||
assert state['conversations'][other] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/conversations')]
|
||||
assert state['artifacts'][other]['id'] not in [x['id'] for x in ok(viewer,'/api/projects/'+HOST+'/artifacts')]
|
||||
foreign='prj_b32c62943fa3';foreign_pid='p-39dd859c3c99'
|
||||
assert foreign not in [x['id'] for x in ok('test','/api/projects')]
|
||||
foreign_status={'conversation_list':deny('test','/api/projects/'+foreign+'/conversations'),'conversation_create':deny('test','/api/projects/'+foreign+'/conversations','POST',{'title':'cross-project denial probe'}),'artifacts':deny('test','/api/projects/'+foreign+'/artifacts')}
|
||||
code,denied=client.request('test','/api/commands/rdm.inspect.summary','POST',{'params':{'projectId':foreign_pid},'entrance':'workspace'})
|
||||
assert code in (400,403,404) and denied.get('receipt',{}).get('state')=='rejected';foreign_status['domain_summary']=code
|
||||
code,_=client.request('test','/api/projects','POST',{'name':'genui-e2e'});assert code==403,'member unexpectedly gained system project-create authority'
|
||||
code,_=client.request('weichen','/api/projects','POST',{'name':'U7宿主项目'});assert code==409,'another owner project name was not globally reserved';duplicate_status=code
|
||||
before=json.loads((W/'baseline-private.json').read_text());current=prepare.logical(prepare.DATA)
|
||||
backup=Path(json.loads((W/'backup-location.json').read_text())['path'])
|
||||
with prepare.db(backup/'dev/data/rdm.db') as c:
|
||||
moved=Counter(prepare.digest(x) for x in c.execute('select * from rdm_members where project_id=?',(HOST,)))
|
||||
preserved={}
|
||||
for table,b in before['logical'].items():
|
||||
expected=Counter(b['rows'])
|
||||
if table=='rdm.db:rdm_members':expected-=moved
|
||||
assert not expected-Counter(current[table]['rows']), 'old business rows changed: '+table
|
||||
preserved[table]={'old_preserved':sum(expected.values()),'current':len(current[table]['rows'])}
|
||||
for a in before['artifacts']:assert prepare.sha(Path(a['content_path']))==a['sha256']
|
||||
result={'source':'SELF','hostProjectId':HOST,'canonicalProjectId':PID,'account_visibility_and_private_creation':'PASS','private_peer_status':private_status,'cross_project_status':foreign_status,'global_duplicate_name_status':duplicate_status,'preserved_business_rows':preserved,'original_artifact_bytes_preserved':len(before['artifacts']),'fixtures':state,'password_login':'NOT_RUN; user selected temporary API credentials'}
|
||||
client.save('acceptance-http.json',result);print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,35 @@
|
||||
import json,sqlite3
|
||||
from pathlib import Path
|
||||
import client,prepare
|
||||
W=client.W;HOST=prepare.HOST
|
||||
preview=json.loads((W/'pre-repair-preview.json').read_text())['preview']
|
||||
assert preview=={'hostProjectId':HOST,'name':'genui-e2e','memberCount':2,'alreadyBound':False}
|
||||
with prepare.db(prepare.DATA/'agent-system.db') as c:
|
||||
p=dict(c.execute('select id,name,owner from projects where id=?',(HOST,)).fetchone())
|
||||
assert p=={'id':HOST,'name':'genui-e2e','owner':'weichen'}
|
||||
assert c.execute('select count(*) from projects where trim(name)=? and id<>?',(p['name'],HOST)).fetchone()[0]==0
|
||||
with prepare.db(prepare.DATA/'rdm.db') as c:
|
||||
assert c.execute('select count(*) from rdm_project_names where name=?',(p['name'],)).fetchone()[0]==0,'reserved domain name conflict'
|
||||
assert c.execute('select count(*) from rdm_projects where name=?',(p['name'],)).fetchone()[0]==0
|
||||
assert c.execute('select count(*) from rdm_host_project_map where host_project=?',(HOST,)).fetchone()[0]==0,'binding changed; reconcile'
|
||||
rows=[dict(x) for x in c.execute('select owner,role from rdm_members where project_id=? order by owner',(HOST,))]
|
||||
assert rows==[{'owner':'test','role':'member'},{'owner':'weichen','role':'member'}]
|
||||
print('PASS pre-apply identity, host/domain name reservation and membership conflict checks',flush=True)
|
||||
out=client.command('weichen','rdm.project.adopt',{'hostProjectId':HOST,'apply':True})
|
||||
assert out['id']==HOST and out['owner']=='weichen' and out['name']=='genui-e2e' and out['settings']['rdm.state']=='ready'
|
||||
canonical=out['settings']['rdm.projectId']
|
||||
with prepare.db(prepare.DATA/'rdm.db') as c:
|
||||
binding=dict(c.execute('select * from rdm_host_project_map where host_project=?',(HOST,)).fetchone())
|
||||
assert binding['rdm_project']==canonical and binding['created_by']=='weichen' and binding['state']=='ready'
|
||||
roles=[dict(x) for x in c.execute('select owner,role from rdm_members where project_id=? order by owner',(canonical,))]
|
||||
assert roles==[{'owner':'test','role':'member'},{'owner':'weichen','role':'owner'}]
|
||||
assert c.execute('select count(*) from rdm_members where project_id=?',(HOST,)).fetchone()[0]==0
|
||||
assert c.execute('select count(*) from rdm_projects where name=?',('genui-e2e',)).fetchone()[0]==1
|
||||
for owner in ['weichen','test']:
|
||||
code,projects=client.request(owner,'/api/projects');assert code==200
|
||||
visible=next(x for x in projects if x['id']==HOST)
|
||||
assert visible['settings']['rdm.projectId']==canonical and visible['settings']['rdm.state']=='ready'
|
||||
repeat=client.command('weichen','rdm.project.adopt',{'hostProjectId':HOST,'apply':True})
|
||||
assert repeat['id']==HOST and repeat['settings']['rdm.projectId']==canonical
|
||||
result={'source':'SELF','hostProjectId':HOST,'name':'genui-e2e','owner':'weichen','canonicalProjectId':canonical,'binding':binding,'members':roles,'both_accounts_see_same_project':True,'repeat_apply_same_identity':True}
|
||||
client.save('adoption-result.json',result);print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"hostProjectId": "prj_8d20020e5b79",
|
||||
"name": "genui-e2e",
|
||||
"owner": "weichen",
|
||||
"canonicalProjectId": "p-aea70b2559c6",
|
||||
"binding": {
|
||||
"host_project": "prj_8d20020e5b79",
|
||||
"rdm_project": "p-aea70b2559c6",
|
||||
"state": "ready",
|
||||
"created_by": "weichen",
|
||||
"created_at": "2026-10-13T16:02:00.170829288Z"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
"owner": "test",
|
||||
"role": "member"
|
||||
},
|
||||
{
|
||||
"owner": "weichen",
|
||||
"role": "owner"
|
||||
}
|
||||
],
|
||||
"both_accounts_see_same_project": true,
|
||||
"repeat_apply_same_identity": true
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"all_original_business_rows_preserved": true,
|
||||
"databases": {
|
||||
"agent-system.db": {
|
||||
"original_tables_checked": 53,
|
||||
"unchanged_original_rows": 88359,
|
||||
"deltas": [
|
||||
{
|
||||
"table": "conversations",
|
||||
"old_rows": 7,
|
||||
"new_rows": 12,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 5
|
||||
},
|
||||
{
|
||||
"table": "artifacts",
|
||||
"old_rows": 14,
|
||||
"new_rows": 16,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 2
|
||||
},
|
||||
{
|
||||
"table": "api_keys",
|
||||
"old_rows": 24,
|
||||
"new_rows": 26,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 2
|
||||
},
|
||||
{
|
||||
"table": "command_audit",
|
||||
"old_rows": 76895,
|
||||
"new_rows": 76935,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 40
|
||||
},
|
||||
{
|
||||
"table": "sessions",
|
||||
"old_rows": 34,
|
||||
"new_rows": 34,
|
||||
"removed_or_changed_old_rows": 1,
|
||||
"new_or_changed_rows": 1
|
||||
},
|
||||
{
|
||||
"table": "audit_events",
|
||||
"old_rows": 86,
|
||||
"new_rows": 96,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 10
|
||||
}
|
||||
],
|
||||
"new_tables": []
|
||||
},
|
||||
"rdm.db": {
|
||||
"original_tables_checked": 25,
|
||||
"unchanged_original_rows": 231,
|
||||
"deltas": [
|
||||
{
|
||||
"table": "rdm_members",
|
||||
"old_rows": 8,
|
||||
"new_rows": 8,
|
||||
"removed_or_changed_old_rows": 2,
|
||||
"new_or_changed_rows": 2
|
||||
},
|
||||
{
|
||||
"table": "rdm_projects",
|
||||
"old_rows": 1,
|
||||
"new_rows": 2,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 1
|
||||
},
|
||||
{
|
||||
"table": "rdm_host_project_map",
|
||||
"old_rows": 1,
|
||||
"new_rows": 2,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 1
|
||||
},
|
||||
{
|
||||
"table": "rdm_role_actions",
|
||||
"old_rows": 1,
|
||||
"new_rows": 2,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 1
|
||||
},
|
||||
{
|
||||
"table": "rdm_account_roles",
|
||||
"old_rows": 1,
|
||||
"new_rows": 2,
|
||||
"removed_or_changed_old_rows": 0,
|
||||
"new_or_changed_rows": 1
|
||||
}
|
||||
],
|
||||
"new_tables": [
|
||||
"rdm_project_names"
|
||||
]
|
||||
}
|
||||
},
|
||||
"auth_session_differences": 1,
|
||||
"notes": [
|
||||
"Only the two previewed host memberships were migrated; existing business rows in all other original tables are retained.",
|
||||
"Session replacement and extra role/action rows were observed while the shared instance was in use; they were left untouched, not attributed to this operation.",
|
||||
"Temporary test keys were revoked, and their plaintext file was removed."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import prepare
|
||||
W=prepare.W;backup=Path(json.loads((W/'backup-location.json').read_text())['path'])
|
||||
result={'source':'SELF','all_original_business_rows_preserved':True,'databases':{},'auth_session_differences':0}
|
||||
for name in ['agent-system.db','rdm.db']:
|
||||
with prepare.db(backup/'dev/data'/name) as a,prepare.db(prepare.DATA/name) as b:
|
||||
old_tables=[r[0] for r in a.execute("select name from sqlite_master where type='table' and name not like 'sqlite_%'")]
|
||||
new_tables=[r[0] for r in b.execute("select name from sqlite_master where type='table' and name not like 'sqlite_%'")]
|
||||
assert set(old_tables)<=set(new_tables),'old table missing'
|
||||
deltas=[];preserved=0
|
||||
for table in old_tables:
|
||||
escaped=table.replace('"','""');cols=[r['name'] for r in a.execute('pragma table_info("'+escaped+'")')]
|
||||
query='select '+','.join('"'+c.replace('"','""')+'"' for c in cols)+' from "'+escaped+'"'
|
||||
old=Counter(prepare.digest(r) for r in a.execute(query));new=Counter(prepare.digest(r) for r in b.execute(query))
|
||||
missing=old-new
|
||||
if table=='sessions':result['auth_session_differences']=sum(missing.values())
|
||||
elif name=='rdm.db' and table=='rdm_members':
|
||||
moved=Counter(prepare.digest(r) for r in a.execute('select * from rdm_members where project_id=?',(prepare.HOST,)))
|
||||
assert missing==moved and sum(moved.values())==2,'unexpected membership removal'
|
||||
else:assert not missing,'old business data changed: '+name+':'+table
|
||||
preserved+=sum(old.values())-sum(missing.values())
|
||||
if missing or new-old:deltas.append({'table':table,'old_rows':sum(old.values()),'new_rows':sum(new.values()),'removed_or_changed_old_rows':sum(missing.values()),'new_or_changed_rows':sum((new-old).values())})
|
||||
result['databases'][name]={'original_tables_checked':len(old_tables),'unchanged_original_rows':preserved,'deltas':deltas,'new_tables':[t for t in new_tables if t not in old_tables]}
|
||||
result['notes']=['Only the two previewed host memberships were migrated; existing business rows in all other original tables are retained.','Session replacement and extra role/action rows were observed while the shared instance was in use; they were left untouched, not attributed to this operation.','Temporary test keys were revoked, and their plaintext file was removed.']
|
||||
p=W/'all-table-preservation.json';p.write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n');p.chmod(0o600)
|
||||
print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,34 @@
|
||||
import json,os,re,sqlite3,subprocess,urllib.request,urllib.error
|
||||
from pathlib import Path
|
||||
W=Path('/tmp/project-member-runtime-8788-20261013');DATA=Path('/home/weichen/projects/Agent-System-Dev/runtime/dev/data')
|
||||
ADMIN=Path('/home/weichen/projects/Agent-System-RDM-wt-feature-project-member-access/bin/agent-system-admin')
|
||||
NAME='project-member-runtime-20261013';HOST='prj_8d20020e5b79';BASE='http://127.0.0.1:8788'
|
||||
assert (W/'backup-proof.json').is_file()
|
||||
assert not (W/'credentials.json').exists(),'credentials already created; reconcile before retry'
|
||||
with sqlite3.connect('file:'+str(DATA/'agent-system.db')+'?mode=ro',uri=True) as c:
|
||||
assert c.execute('select max(version) from schema_migrations').fetchone()[0]==30
|
||||
assert c.execute('select count(*) from api_keys where name=? and revoked=0',(NAME,)).fetchone()[0]==0
|
||||
assert c.execute("select count(*) from users where owner in ('weichen','test')").fetchone()[0]==2
|
||||
creds={};p=W/'credentials.json'
|
||||
env=dict(os.environ);env.pop('AGENT_SYSTEM_DSN',None)
|
||||
for owner in ['weichen','test']:
|
||||
r=subprocess.run([str(ADMIN),'apikey','create','--owner',owner,'--name',NAME,'--data',str(DATA)],capture_output=True,text=True,env=env)
|
||||
assert r.returncode==0, 'temporary credential creation failed'
|
||||
key=r.stdout.strip();assert re.fullmatch(r'ask_[A-Za-z0-9_-]+',key),'unexpected credential format'
|
||||
creds[owner]=key;p.write_text(json.dumps(creds)+'\n');p.chmod(0o600)
|
||||
def req(owner,path,method='GET',body=None):
|
||||
r=urllib.request.Request(BASE+path,method=method,data=None if body is None else json.dumps(body).encode(),headers={'Authorization':'Bearer '+creds[owner],'Content-Type':'application/json','Origin':BASE})
|
||||
try:
|
||||
with urllib.request.urlopen(r,timeout=20) as x:return x.status,json.load(x)
|
||||
except urllib.error.HTTPError as e:return e.code,json.loads(e.read())
|
||||
result={}
|
||||
for owner in creds:
|
||||
status,me=req(owner,'/api/auth/me');assert status==200 and me['owner']==owner
|
||||
status,projects=req(owner,'/api/projects');assert status==200
|
||||
result[owner]={'authenticated_as':me['owner'],'visible_project_ids':[x['id'] for x in projects]}
|
||||
assert HOST in result['weichen']['visible_project_ids'] and HOST not in result['test']['visible_project_ids']
|
||||
status,_=req('test','/api/projects/'+HOST+'/conversations','POST',{'title':'membership pre-deploy denial probe'})
|
||||
assert status in (403,404),'old member create unexpectedly admitted; reconcile new test data'
|
||||
result['member_create_before_status']=status
|
||||
(W/'before-http.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n')
|
||||
print(json.dumps({'source':'SELF','temporary_credentials':'created for existing weichen/test; values protected; passwords unchanged','before':result},ensure_ascii=False))
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"backup": "/home/weichen/projects/Agent-System-Dev/runtime-backups/dev/project-member-20261013T154718Z",
|
||||
"instance": "svc-dev-328312de05f9",
|
||||
"old_release_retained": "releases/rdm-84ca64f-20261013T143811Z-2735902",
|
||||
"runtime_directories": [
|
||||
"dev",
|
||||
"devweb"
|
||||
],
|
||||
"full_release_count": 6,
|
||||
"restored_databases": [
|
||||
"agent-system.db",
|
||||
"rdm.db"
|
||||
],
|
||||
"integrity_check": "ok",
|
||||
"logical_rows_match": true,
|
||||
"artifact_bytes_match": 14,
|
||||
"manifest_sha256": "5ed67a4d7bf7d59668e7a2dccb59358086f10559cd4c69079285f1bd2f6b0f63"
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { chromium, expect } from '/home/weichen/projects/Agent-System-Dev-wt-feature-project-member-access/modules/frontend/node_modules/@playwright/test/index.mjs'
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
const W='/tmp/project-member-runtime-8788-20261013', base='http://127.0.0.1:8788'
|
||||
const creds=JSON.parse(fs.readFileSync(W+'/credentials.json','utf8'))
|
||||
const adoption=JSON.parse(fs.readFileSync(W+'/adoption-result.json','utf8'))
|
||||
const host=adoption.hostProjectId, domain=adoption.canonicalProjectId
|
||||
fs.mkdirSync(W+'/screenshots',{recursive:true,mode:0o700})
|
||||
const errors=[], result={source:'SELF',authentication:'temporary API credentials; existing passwords unchanged; password login NOT_RUN',hostProjectId:host,canonicalProjectId:domain,accounts:{}}
|
||||
const browser=await chromium.launch({headless:true})
|
||||
const context=await browser.newContext({viewport:{width:1440,height:1000},locale:'zh-CN'})
|
||||
const page=await context.newPage()
|
||||
page.on('pageerror',e=>errors.push(e.message))
|
||||
async function api(path,method='GET',body){
|
||||
return await page.evaluate(async ({path,method,body})=>{
|
||||
const headers={Authorization:'Bearer '+localStorage.getItem('agent-system.apiKey')}
|
||||
if(body!==undefined)headers['Content-Type']='application/json'
|
||||
const r=await fetch(path,{method,headers,body:body===undefined?undefined:JSON.stringify(body)})
|
||||
const text=await r.text();return {status:r.status,body:text?JSON.parse(text):null}
|
||||
},{path,method,body})
|
||||
}
|
||||
try{
|
||||
await page.goto(base,{waitUntil:'domcontentloaded'})
|
||||
await expect(page.getByRole('button',{name:'登录',exact:true})).toBeVisible()
|
||||
for(const owner of ['weichen','test']){
|
||||
const other=owner==='weichen'?'test':'weichen'
|
||||
await page.evaluate(key=>localStorage.setItem('agent-system.apiKey',key),creds[owner])
|
||||
await page.reload({waitUntil:'domcontentloaded'})
|
||||
await expect(page.locator('.tsb-projrow-name')).toHaveText('genui-e2e',{timeout:20000})
|
||||
const me=await api('/api/me');expect(me.status).toBe(200);expect(me.body.owner).toBe(owner)
|
||||
const projects=await api('/api/projects');expect(projects.status).toBe(200)
|
||||
expect(projects.body.map(p=>p.id)).toEqual([host]);expect(projects.body[0].settings['rdm.projectId']).toBe(domain)
|
||||
await expect(page.locator('.tsb-task-t').filter({hasText:'成员协作复测-'+owner+'-20261013'})).toBeVisible()
|
||||
await expect(page.locator('.tsb-task-t').filter({hasText:'成员协作复测-'+other+'-20261013'})).toHaveCount(0)
|
||||
await page.locator('.tsb-projrow').click()
|
||||
await expect(page.locator('.tsb-mi').filter({hasText:'U7宿主项目'})).toHaveCount(0)
|
||||
await page.locator('.tsb-mi').filter({hasText:'genui-e2e'}).click()
|
||||
const waiting=page.waitForResponse(r=>new URL(r.url()).pathname==='/api/projects/'+host+'/conversations'&&r.request().method()==='POST')
|
||||
await page.getByRole('button',{name:'新建对话',exact:true}).click()
|
||||
const response=await waiting;expect(response.status()).toBe(200)
|
||||
const conv=await response.json();expect(conv.owner).toBe(owner);expect(conv.projectId).toBe(host)
|
||||
result.accounts[owner]={conversationCreatedByUI:conv.id,projectVisible:true,otherAccountConversationAbsent:true}
|
||||
fs.writeFileSync(W+'/browser-progress.json',JSON.stringify(result,null,2),{mode:0o600})
|
||||
const title='成员协作浏览器复测-'+owner+'-20261013'
|
||||
expect((await api('/api/conversations/'+conv.id,'PATCH',{title})).status).toBe(200)
|
||||
await page.reload({waitUntil:'domcontentloaded'})
|
||||
await expect(page.locator('.tsb-projrow-name')).toHaveText('genui-e2e',{timeout:20000})
|
||||
await expect(page.locator('.tsb-task-t').filter({hasText:title})).toBeVisible()
|
||||
await page.getByTitle('文件搜索、预览、编辑与下载').click()
|
||||
await expect(page.locator('.artifacts-panel')).toBeVisible()
|
||||
await expect(page.locator('.artifacts-panel .name').filter({hasText:'成员协作复测-'+owner+'-私有.txt'})).toBeVisible()
|
||||
await expect(page.locator('.artifacts-panel .name').filter({hasText:'成员协作复测-'+other+'-私有.txt'})).toHaveCount(0)
|
||||
await page.screenshot({path:W+'/screenshots/'+owner+'-private-project.png'})
|
||||
Object.assign(result.accounts[owner],{refreshPassed:true,ownFileVisible:true,otherAccountFileAbsent:true})
|
||||
await page.locator('.tsb-user').click()
|
||||
await expect(page.getByText('租户:'+owner,{exact:true})).toBeVisible()
|
||||
await page.getByRole('button',{name:'退出登录',exact:true}).click()
|
||||
await expect(page.getByRole('button',{name:'登录',exact:true})).toBeVisible({timeout:15000})
|
||||
expect(await page.evaluate(()=>localStorage.getItem('agent-system.apiKey'))).toBeNull()
|
||||
}
|
||||
expect(errors).toEqual([])
|
||||
result.pageerrorCount=errors.length;result.accountSwitchThroughLogout=true
|
||||
result.driverSHA256=crypto.createHash('sha256').update(fs.readFileSync(W+'/browser.mjs')).digest('hex')
|
||||
fs.writeFileSync(W+'/acceptance-browser.json',JSON.stringify(result,null,2),{mode:0o600})
|
||||
console.log(JSON.stringify(result))
|
||||
}finally{await context.close();await browser.close()}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"authentication": "temporary API credentials; existing passwords unchanged; password login NOT_RUN",
|
||||
"hostProjectId": "prj_8d20020e5b79",
|
||||
"canonicalProjectId": "p-aea70b2559c6",
|
||||
"accounts": {
|
||||
"weichen": {
|
||||
"conversationCreatedByUI": "cnv_53ed2ce8df0d",
|
||||
"projectVisible": true,
|
||||
"otherAccountConversationAbsent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { chromium, expect } from '/home/weichen/projects/Agent-System-Dev-wt-feature-project-member-access/modules/frontend/node_modules/@playwright/test/index.mjs'
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
const W='/tmp/project-member-runtime-8788-20261013', base='http://127.0.0.1:8788'
|
||||
const creds=JSON.parse(fs.readFileSync(W+'/credentials.json','utf8'))
|
||||
const adoption=JSON.parse(fs.readFileSync(W+'/adoption-result.json','utf8'))
|
||||
const host=adoption.hostProjectId, domain=adoption.canonicalProjectId
|
||||
fs.mkdirSync(W+'/screenshots',{recursive:true,mode:0o700})
|
||||
const errors=[], result={source:'SELF',authentication:'temporary API credentials; existing passwords unchanged; password login NOT_RUN',hostProjectId:host,canonicalProjectId:domain,accounts:{}}
|
||||
const browser=await chromium.launch({headless:true})
|
||||
const context=await browser.newContext({viewport:{width:1440,height:1000},locale:'zh-CN'})
|
||||
const page=await context.newPage()
|
||||
page.on('pageerror',e=>errors.push(e.message))
|
||||
async function api(path,method='GET',body){
|
||||
return await page.evaluate(async ({path,method,body})=>{
|
||||
const headers={Authorization:'Bearer '+localStorage.getItem('agent-system.apiKey')}
|
||||
if(body!==undefined)headers['Content-Type']='application/json'
|
||||
const r=await fetch(path,{method,headers,body:body===undefined?undefined:JSON.stringify(body)})
|
||||
const text=await r.text();return {status:r.status,body:text?JSON.parse(text):null}
|
||||
},{path,method,body})
|
||||
}
|
||||
try{
|
||||
await page.goto(base,{waitUntil:'domcontentloaded'})
|
||||
await expect(page.getByRole('button',{name:'登录',exact:true})).toBeVisible()
|
||||
for(const owner of ['weichen','test']){
|
||||
const other=owner==='weichen'?'test':'weichen'
|
||||
await page.evaluate(key=>localStorage.setItem('agent-system.apiKey',key),creds[owner])
|
||||
await page.reload({waitUntil:'domcontentloaded'})
|
||||
await expect(page.locator('.tsb-projrow-name')).toHaveText('genui-e2e',{timeout:20000})
|
||||
const me=await api('/api/me');expect(me.status).toBe(200);expect(me.body.owner).toBe(owner)
|
||||
const projects=await api('/api/projects');expect(projects.status).toBe(200)
|
||||
expect(projects.body.map(p=>p.id)).toEqual([host]);expect(projects.body[0].settings['rdm.projectId']).toBe(domain)
|
||||
await expect(page.locator('.tsb-task-t').filter({hasText:'成员协作复测-'+owner+'-20261013'})).toBeVisible()
|
||||
await expect(page.locator('.tsb-task-t').filter({hasText:'成员协作复测-'+other+'-20261013'})).toHaveCount(0)
|
||||
await page.locator('.tsb-projrow').click()
|
||||
await expect(page.locator('.tsb-mi').filter({hasText:'U7宿主项目'})).toHaveCount(0)
|
||||
await page.locator('.tsb-mi').filter({hasText:'genui-e2e'}).click()
|
||||
const waiting=page.waitForResponse(r=>new URL(r.url()).pathname==='/api/projects/'+host+'/conversations'&&r.request().method()==='POST')
|
||||
await page.getByRole('button',{name:'新建对话',exact:true}).click()
|
||||
const response=await waiting;expect(response.status()).toBe(200)
|
||||
const conv=await response.json();expect(conv.owner).toBe(owner);expect(conv.projectId).toBe(host)
|
||||
result.accounts[owner]={conversationCreatedByUI:conv.id,projectVisible:true,otherAccountConversationAbsent:true}
|
||||
fs.writeFileSync(W+'/browser-progress.json',JSON.stringify(result,null,2),{mode:0o600})
|
||||
const title='成员协作浏览器复测-'+owner+'-20261013-'+conv.id.slice(-6)
|
||||
expect((await api('/api/conversations/'+conv.id,'PATCH',{title})).status).toBe(200)
|
||||
await page.reload({waitUntil:'domcontentloaded'})
|
||||
await expect(page.locator('.tsb-projrow-name')).toHaveText('genui-e2e',{timeout:20000})
|
||||
await expect(page.locator('.tsb-task-t').filter({hasText:title})).toBeVisible()
|
||||
await page.locator('.tsb-tool[title="文件搜索、预览、编辑与下载"]').click()
|
||||
await expect(page.locator('.artifacts-panel')).toBeVisible()
|
||||
await expect(page.locator('.artifacts-panel .name').filter({hasText:'成员协作复测-'+owner+'-私有.txt'})).toBeVisible()
|
||||
await expect(page.locator('.artifacts-panel .name').filter({hasText:'成员协作复测-'+other+'-私有.txt'})).toHaveCount(0)
|
||||
await page.screenshot({path:W+'/screenshots/'+owner+'-private-project.png'})
|
||||
Object.assign(result.accounts[owner],{refreshPassed:true,ownFileVisible:true,otherAccountFileAbsent:true})
|
||||
await page.locator('.tsb-user').click()
|
||||
await expect(page.getByText('租户:'+owner,{exact:true})).toBeVisible()
|
||||
await page.getByRole('button',{name:'退出登录',exact:true}).click()
|
||||
await expect(page.getByRole('button',{name:'登录',exact:true})).toBeVisible({timeout:15000})
|
||||
expect(await page.evaluate(()=>localStorage.getItem('agent-system.apiKey'))).toBeNull()
|
||||
}
|
||||
expect(errors).toEqual([])
|
||||
result.pageerrorCount=errors.length;result.accountSwitchThroughLogout=true
|
||||
result.driverSHA256=crypto.createHash('sha256').update(fs.readFileSync(W+'/browser.mjs')).digest('hex')
|
||||
fs.writeFileSync(W+'/acceptance-browser.json',JSON.stringify(result,null,2),{mode:0o600})
|
||||
console.log(JSON.stringify(result))
|
||||
}finally{await context.close();await browser.close()}
|
||||
@@ -0,0 +1,21 @@
|
||||
import json,urllib.request,urllib.error
|
||||
from pathlib import Path
|
||||
W=Path('/tmp/project-member-runtime-8788-20261013');BASE='http://127.0.0.1:8788'
|
||||
CREDS=json.loads((W/'credentials.json').read_text())
|
||||
def request(owner,path,method='GET',data=None,raw=False,headers=None):
|
||||
h={'Authorization':'Bearer '+CREDS[owner],'Origin':BASE}
|
||||
if headers:h.update(headers)
|
||||
if data is not None and not isinstance(data,bytes):data=json.dumps(data).encode();h['Content-Type']='application/json'
|
||||
r=urllib.request.Request(BASE+path,method=method,data=data,headers=h)
|
||||
try:
|
||||
with urllib.request.urlopen(r,timeout=30) as x:code=x.status;body=x.read()
|
||||
except urllib.error.HTTPError as e:code=e.code;body=e.read()
|
||||
if raw:return code,body
|
||||
try:return code,json.loads(body)
|
||||
except (ValueError,UnicodeDecodeError):return code,{'non_json_bytes':len(body)}
|
||||
def command(owner,name,params):
|
||||
code,out=request(owner,'/api/commands/'+name,'POST',{'params':params,'entrance':'workspace'})
|
||||
assert code==200 and out.get('receipt',{}).get('state')=='committed',f'{name} rejected status={code}'
|
||||
return out['payload']
|
||||
def save(name,obj):
|
||||
p=W/name;p.write_text(json.dumps(obj,ensure_ascii=False,indent=2)+'\n');p.chmod(0o600)
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"operation": "succeeded",
|
||||
"pid": 3896104,
|
||||
"release": "releases/rdm-3f647f6-20261013T155418Z-3842908",
|
||||
"binary_sha256": "d5db3d0ac292d99582ec91938317af356c050ad61e61f42c097e7faf7a731765",
|
||||
"instance": "svc-dev-328312de05f9",
|
||||
"root_http": 200,
|
||||
"temporary_keys_revoked": {
|
||||
"weichen": 401,
|
||||
"test": 401
|
||||
},
|
||||
"plaintext_credential_file_removed": true,
|
||||
"existing_accounts_and_passwords_unchanged": true,
|
||||
"original_artifact_bytes_preserved": 14,
|
||||
"auth_route": "temporary API credentials; password login NOT_RUN",
|
||||
"verification_data_retained": {
|
||||
"empty_conversations": 5,
|
||||
"synthetic_private_files": 2
|
||||
},
|
||||
"worktrees_retained": true
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import json,os,urllib.request,urllib.error
|
||||
from pathlib import Path
|
||||
import prepare
|
||||
r=prepare.R;w=prepare.W
|
||||
pid=int((r/'svc.pid').read_text());exe=Path(f'/proc/{pid}/exe').resolve()
|
||||
assert exe==(r/'current/bin/agent-system').resolve()
|
||||
assert prepare.sha(exe)=='d5db3d0ac292d99582ec91938317af356c050ad61e61f42c097e7faf7a731765'
|
||||
assert os.readlink(r/'current').startswith('releases/rdm-3f647f6-')
|
||||
assert json.loads((r/'manifest.json').read_text())['instance_id']==prepare.INSTANCE
|
||||
with prepare.db(prepare.DATA/'rdm.db') as c:
|
||||
b=dict(c.execute('select * from rdm_host_project_map where host_project=?',(prepare.HOST,)).fetchone())
|
||||
assert b['rdm_project']=='p-aea70b2559c6' and b['state']=='ready' and b['created_by']=='weichen'
|
||||
roles=[tuple(x) for x in c.execute('select owner,role from rdm_members where project_id=? order by owner',(b['rdm_project'],))]
|
||||
assert roles==[('test','member'),('weichen','owner')]
|
||||
with prepare.db(prepare.DATA/'agent-system.db') as c:
|
||||
assert c.execute("select count(*) from api_keys where name='project-member-runtime-20261013' and revoked=0").fetchone()[0]==0
|
||||
assert not (w/'credentials.json').exists()
|
||||
with urllib.request.urlopen('http://127.0.0.1:8788/',timeout=15) as response:assert response.status==200
|
||||
try:urllib.request.urlopen('http://127.0.0.1:8788/api/me',timeout=15);raise AssertionError('anonymous authenticated')
|
||||
except urllib.error.HTTPError as error:assert error.code==401
|
||||
print(json.dumps({'source':'SELF','pid':pid,'release':os.readlink(r/'current'),'binary_sha256':prepare.sha(exe),'instance':prepare.INSTANCE,'binding':b,'roles':roles,'root_status':200,'anonymous_me_status':401,'revocation_verified':True,'protected_plaintext_removed':True,'fixed_runtime_object_unchanged_after_record_sync':True},ensure_ascii=False))
|
||||
@@ -0,0 +1,31 @@
|
||||
import hashlib,json,os,subprocess,urllib.request
|
||||
from pathlib import Path
|
||||
import client,prepare
|
||||
W=client.W;R=prepare.R;DATA=prepare.DATA
|
||||
assert (W/'acceptance-http.json').is_file() and (W/'acceptance-browser.json').is_file()
|
||||
assert Path('/tmp/project-member-runtime-independent-review.md').is_file()
|
||||
ADMIN=Path('/home/weichen/projects/Agent-System-RDM-wt-feature-project-member-access/bin/agent-system-admin')
|
||||
NAME='project-member-runtime-20261013'
|
||||
env=dict(os.environ);env.pop('AGENT_SYSTEM_DSN',None)
|
||||
results={}
|
||||
for owner in ['weichen','test']:
|
||||
with prepare.db(DATA/'agent-system.db') as c:
|
||||
row=c.execute('select revoked from api_keys where owner=? and name=?',(owner,NAME)).fetchone();assert row is not None
|
||||
if not row['revoked']:
|
||||
r=subprocess.run([str(ADMIN),'apikey','revoke','--owner',owner,'--name',NAME,'--data',str(DATA)],capture_output=True,text=True,env=env)
|
||||
assert r.returncode==0,'credential revocation failed; reconcile before retry'
|
||||
code,_=client.request(owner,'/api/me');assert code==401,'revoked credential still admitted'
|
||||
results[owner]=code
|
||||
with prepare.db(DATA/'agent-system.db') as c:
|
||||
assert c.execute('select count(*) from api_keys where name=? and revoked=0',(NAME,)).fetchone()[0]==0
|
||||
with urllib.request.urlopen(client.BASE+'/',timeout=15) as res:assert res.status==200
|
||||
before=json.loads((W/'baseline-private.json').read_text());now=prepare.logical(DATA)
|
||||
assert now['agent-system.db:users']==before['logical']['agent-system.db:users'],'existing account/password state changed'
|
||||
for a in before['artifacts']:assert prepare.sha(Path(a['content_path']))==a['sha256']
|
||||
assert json.loads((R/'manifest.json').read_text())['instance_id']==prepare.INSTANCE
|
||||
pid=int((R/'svc.pid').read_text());exe=Path(f'/proc/{pid}/exe').resolve()
|
||||
assert exe==(R/'current/bin/agent-system').resolve()
|
||||
assert prepare.sha(exe)=='d5db3d0ac292d99582ec91938317af356c050ad61e61f42c097e7faf7a731765'
|
||||
(W/'credentials.json').unlink()
|
||||
result={'source':'SELF','operation':'succeeded','pid':pid,'release':os.readlink(R/'current'),'binary_sha256':prepare.sha(exe),'instance':prepare.INSTANCE,'root_http':200,'temporary_keys_revoked':results,'plaintext_credential_file_removed':True,'existing_accounts_and_passwords_unchanged':True,'original_artifact_bytes_preserved':len(before['artifacts']),'auth_route':'temporary API credentials; password login NOT_RUN','verification_data_retained':{'empty_conversations':5,'synthetic_private_files':2},'worktrees_retained':True}
|
||||
client.save('final-state.json',result);print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"conversations": {
|
||||
"weichen": "cnv_7c6d2eb980cc",
|
||||
"test": "cnv_dbae0c998eec"
|
||||
},
|
||||
"artifacts": {
|
||||
"weichen": {
|
||||
"id": "art_11ea0ec72767",
|
||||
"sha256": "816a6cbb70fcc550b860de78cf494f9d3bfd8f77a32c48d53a5dfd83359c1dfb"
|
||||
},
|
||||
"test": {
|
||||
"id": "art_922d016bf0a0",
|
||||
"sha256": "0d894a02ee84e7ba8122f06bf3a3f4d2c0d4c2ba0bab2575d8db285c9d158a3a"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
[
|
||||
{
|
||||
"artifact": "prepare.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/prepare.py",
|
||||
"sha256": "1aa3456744928a5826a3307bf773e48a99100b862f43e48e792ce7955107ffe6"
|
||||
},
|
||||
{
|
||||
"artifact": "auth_before.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/auth_before.py",
|
||||
"sha256": "b339053f774465031205840dec23cea8720f176c26ad46d46261002fd333d317"
|
||||
},
|
||||
{
|
||||
"artifact": "client.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/client.py",
|
||||
"sha256": "060146ec7a2c23dcc1873a018536422d6779e610df8eb7486b2d8719b023ca45"
|
||||
},
|
||||
{
|
||||
"artifact": "pre_repair.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/pre_repair.py",
|
||||
"sha256": "8a04b44489d25d5a2f99cdf911b310c36d3dda7688716d312ccfac4fd0c6ee3f"
|
||||
},
|
||||
{
|
||||
"artifact": "adopt.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/adopt.py",
|
||||
"sha256": "74a4a45db8afc4424fe78af014531bcdfe32a3d2707d1f3591096553e58b7aa4"
|
||||
},
|
||||
{
|
||||
"artifact": "acceptance_http.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/acceptance_http.py",
|
||||
"sha256": "3d4c995002bc944f9697255cc314a275d9fe1418eecb835e0939287c36f51e64"
|
||||
},
|
||||
{
|
||||
"artifact": "acceptance_http-first-attempt.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/acceptance_http-first-attempt.py",
|
||||
"sha256": "8fd08a270270c055350d282cf4a67876a3e65e7878d6ef6171a346a2d5b70071"
|
||||
},
|
||||
{
|
||||
"artifact": "acceptance_http-second-attempt.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/acceptance_http-second-attempt.py",
|
||||
"sha256": "227b4e9587f684ec47fcb647254819981f3983fba2a5dd7ad7d4e7f2d5d76e2c"
|
||||
},
|
||||
{
|
||||
"artifact": "browser.mjs",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/browser.mjs",
|
||||
"sha256": "d8b6b344a29c8914367e9530e74ebdf027563859efa3809110b82335d97f9f3b"
|
||||
},
|
||||
{
|
||||
"artifact": "browser-first-attempt.mjs",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/browser-first-attempt.mjs",
|
||||
"sha256": "3c48c89efbed139790e5c8a62e7df1bf9a34bc5d1fdd1cb5584475f379bb0304"
|
||||
},
|
||||
{
|
||||
"artifact": "finalize.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/finalize.py",
|
||||
"sha256": "6d2b80aaec325d4a05308fd0e7b331fe08853bf4c2c798209df1a2b146f289f0"
|
||||
},
|
||||
{
|
||||
"artifact": "audit_all.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/audit_all.py",
|
||||
"sha256": "02d0e618d3a660e93a830d36a5ee85fcf81ad3cffb3f5b49f2fd36023cf673c9"
|
||||
},
|
||||
{
|
||||
"artifact": "operation.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/operation.json",
|
||||
"sha256": "870057794fa1fe32b98dc5b033d4f6d4d6c34708115d774c6a653677a5dcc300"
|
||||
},
|
||||
{
|
||||
"artifact": "backup-proof.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/backup-proof.json",
|
||||
"sha256": "98e1471900efc9cb0dc69fca794fe2360a3eca7e323db524774554957310edcd"
|
||||
},
|
||||
{
|
||||
"artifact": "pre-repair-preview.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/pre-repair-preview.json",
|
||||
"sha256": "b36a0551a0124070d1e3345773e60fc6be99c45041d0a2dd1192f77d69d05be1"
|
||||
},
|
||||
{
|
||||
"artifact": "adoption-result.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/adoption-result.json",
|
||||
"sha256": "c7d2e5109eab686cdf1a83a03e73eb67dd9a0e882eed78927c9df6f0ed3dae47"
|
||||
},
|
||||
{
|
||||
"artifact": "acceptance-http.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/acceptance-http.json",
|
||||
"sha256": "ebbcc0c7c39593c081ac272a719be5965f4abc8149600081950340d757214380"
|
||||
},
|
||||
{
|
||||
"artifact": "acceptance-browser.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/acceptance-browser.json",
|
||||
"sha256": "c117285f82cf318053156501ad477988f26d54f42bebc94bf5173f3de9a0816e"
|
||||
},
|
||||
{
|
||||
"artifact": "http-fixtures.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/http-fixtures.json",
|
||||
"sha256": "d704c41c94106c5e6d6edd056e48033a6639d1fe5d571748c89ff4f6999f96f6"
|
||||
},
|
||||
{
|
||||
"artifact": "browser-progress-first-attempt.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/browser-progress-first-attempt.json",
|
||||
"sha256": "4fb7f942d5392a2c2711e739e3e0c3e544051e8b78bbde94cbb807a9a1e38676"
|
||||
},
|
||||
{
|
||||
"artifact": "final-state.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/final-state.json",
|
||||
"sha256": "7fa895a2303aa2ea27c3e3d870fc6229a8806e96a0df6b97b9eb628fb5bad57e"
|
||||
},
|
||||
{
|
||||
"artifact": "all-table-preservation.json",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/all-table-preservation.json",
|
||||
"sha256": "406352842d316e6f82150c68aca3227dc5c7788aa179cbc5f3736fd29ac4473b"
|
||||
},
|
||||
{
|
||||
"artifact": "project-member-runtime-independent-review.md",
|
||||
"original": "/tmp/project-member-runtime-independent-review.md",
|
||||
"sha256": "7de2253c9d4cc0cf6bf4de0967f7a101778d20e0c036f3b6df554ecc34dc70e4"
|
||||
},
|
||||
{
|
||||
"artifact": "project-member-runtime-independent-replay.json",
|
||||
"original": "/tmp/project-member-runtime-independent-replay.json",
|
||||
"sha256": "f025cedf0b92cae644852593436af7a56bab80ee06e522ba0635b367c7c31f9c"
|
||||
},
|
||||
{
|
||||
"artifact": "project-member-runtime-independent-comparison-receipt.json",
|
||||
"original": "/tmp/project-member-runtime-independent-comparison-receipt.json",
|
||||
"sha256": "aa4e03d806cc1a4ea0e5a0c249bb9322366b422a181c53b40062ba58c99b3d90"
|
||||
},
|
||||
{
|
||||
"artifact": "project-member-runtime-independent-attestation.json",
|
||||
"original": "/tmp/project-member-runtime-independent-attestation.json",
|
||||
"sha256": "a66ed3928a9f004233f49d67338412d688a5dd2e69c4cbac7ffedb8a3648bcd4"
|
||||
},
|
||||
{
|
||||
"artifact": "final_live_check.py",
|
||||
"original": "/tmp/project-member-runtime-8788-20261013/final_live_check.py",
|
||||
"sha256": "c9e9b0a2fdf1c072b1669e2575e72dfcdfdfa17b53b19d079357b08307c2d081"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
# 8788 项目成员修复运行交付
|
||||
|
||||
状态:已部署、已修复、运行验收通过。记录时间:2026-10-14T00:45:11.760482+08:00(Asia/Shanghai)。
|
||||
|
||||
用户授权本次保数据部署、旧绑定预览及无冲突后的修复、weichen/test 复测;用户另选临时 API 凭据,不改现有密码。密码登录链路为 NOT_RUN。
|
||||
|
||||
## 实际对象
|
||||
|
||||
| 对象 | 结果 |
|
||||
|---|---|
|
||||
| 实例 | `svc-dev-328312de05f9`,`127.0.0.1:8788` |
|
||||
| RDM 固定提交 | `3f647f6c547d3f0d5ab65310cc5f3eabf66e654f` |
|
||||
| 组件 tag | Backend/Core/Frontend/UI `v0.4.0` |
|
||||
| Backend | `5d5fc9891c0eb8994545ccb8b740535d9b99fb0e` |
|
||||
| Core | `5773e74086791731059730bd3726dc8c9cd1381f` |
|
||||
| Frontend | `eb2d6d32f9323094d568bc9d80b4e5ed4f0af25b` |
|
||||
| UI | `2c4322ef56df64ad72f65907d25b048a74bb5ffc` |
|
||||
| 实际 release | `releases/rdm-3f647f6-20261013T155418Z-3842908` |
|
||||
| 实际二进制 SHA256 | `d5db3d0ac292d99582ec91938317af356c050ad61e61f42c097e7faf7a731765` |
|
||||
| 最终验收 PID | `3896104`,`/proc` executable 与 release 一致 |
|
||||
| 数据 | 原 `runtime/dev/data/agent-system.db` 与 `rdm.db`;未换库、未清库 |
|
||||
|
||||
实际二进制与已验证默认组合产物字节一致。部署从固定提交物化四个 pins 并构建完整前端嵌入产物;devweb 伴生工作台也已由现有 svc 流程更新。源码和 tag 的审阅/CI 沿用已固定对象,[RDM CI2977](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-RDM/actions/runs/2977) 已在本任务重新核验;运行结果为 SELF,独立只读重放身份单独记录。
|
||||
|
||||
## 备份与修复
|
||||
|
||||
配对冷备份:`/home/weichen/projects/Agent-System-Dev/runtime-backups/dev/project-member-20261013T154718Z`。保留 dev/devweb 配置、两库、14 个原件以及全部 6 份旧 release。在隔离恢复副本实际打开两库,integrity_check 与原始业务行对账通过,原件摘要一致;备份包含完整清单摘要。
|
||||
|
||||
旧宿主 `prj_8d20020e5b79`(`genui-e2e`,owner=weichen)保持 ID、名称、所有者不变。预览显示两个孤立成员、无绑定;再次查询物理所有者、宿主/领域名称及名称预留、绑定和成员后确认无冲突,才调用 apply。
|
||||
|
||||
形成唯一领域根 `p-aea70b2559c6`,映射 ready。原两条孤立成员迁到该领域根:weichen 为 owner,test 为 member。重复 apply 返回相同身份,没有重复项目。其他旧项目、绑定和成员保留。
|
||||
|
||||
## 运行验证
|
||||
|
||||
| 场景 | 结果 |
|
||||
|---|---|
|
||||
| 修复前同一新制品 | test 无目标项目,新建会话 404;预览未改业务行 |
|
||||
| 两账户可见性 | 均看到同一宿主/领域 ID |
|
||||
| 新建对话 | HTTP 和实际浏览器按钮均成功,owner 为当前账户 |
|
||||
| 私人会话与文件 | 本人可读;对方直接读取 404,列表不暴露 |
|
||||
| 跨项目隔离 | test 对 U7 会话列表、新建会话、工件均 404;领域摘要命令被拒 |
|
||||
| 全局同名 | 有创建权限的 weichen 创建另一账号已有的 U7 宿主项目名称,409 |
|
||||
| 权限分离 | test 可在已加入项目建会话,但没有系统级创建项目权限,403 |
|
||||
| 浏览器 | weichen→退出→test→退出、刷新、私人文件列表通过;pageerror=0 |
|
||||
| 原始数据 | 原12账户、3宿主项目、7会话、35消息、14工件与全部原件字节保留;原其他项目成员未变 |
|
||||
| 最终凭据 | 两枚临时 key 已撤销,分别实测401,明文文件已删除;原账户与密码字段不变 |
|
||||
|
||||
全表对账覆盖原基座53张表及RDM25张表。除两条目标孤立成员的预期迁移外,旧业务行保留。共享实例使用期间观察到一条登录会话替换以及额外角色/权限行增加,保持现状,不归因本次操作,不以旧备份覆盖这些新增状态。
|
||||
|
||||
本次保留5条有“成员协作”标记的验收空会话及2个合成私有文件,未发送模型消息。历史会话和原件未删除。临时凭据生命周期结束,密码登录按用户选择未测试。
|
||||
|
||||
## 证据和限制
|
||||
|
||||
完整命令收据由任务卡中的 evidence 工具生成,来源保持 SELF。独立审阅者 `/root/runtime_acceptance_review` 先独立重放再对照作者记录,结论 PASS;其原始收据、attestation 和报告在本目录。浏览器由本次操作者执行,独立审阅核对报告范围,未再次创建数据或重跑 UI。
|
||||
|
||||
截图保存在受保护目录:`/tmp/project-member-runtime-8788-20261013/screenshots/test-private-project.png` 与 `weichen-private-project.png`,未放入 Git。
|
||||
|
||||
早期探针失败均保留:身份接口误写为 `/api/auth/me`,旧版本 HTTP 对照未完成;后改用真实 `/api/me` 在新制品上验证绑定修复前后。上传应答被误按数组解析,回读已成功上传的工件后复用,没有重复上传。全局名称测试最初使用无创建权限的 test,改用有权限的 weichen 验证。浏览器文件入口定位命中两个同名按钮,改为明确侧栏入口后完整重跑通过。均为探针问题,未据此修改产品代码或伪记 PASS。
|
||||
|
||||
服务 history 记录部署启动后现有 recover 流程再次拉起进程;所有最终 HTTP、浏览器及独立验收均针对最终 PID 和上述同一制品完成。未推断 recover 的原因,未扩大修改服务管理实现。
|
||||
|
||||
操作完成后服务保持运行,工作区和分支保留。没有新增产品源码改动、tag 或产品 PR;本任务的运行记录保存在独立工作区,尚未合入 main。后续可用原有账户进行常规密码登录验收;本次已选的账户身份功能复测无剩余阻塞。
|
||||
|
||||
这些脚本是本次已执行操作的历史证据,含明确的 live 写入步骤;不能未经新的操作授权直接重跑。私有业务副本与认证材料未纳入本目录,原始文件 byte/hash 对照见 manifest.json。
|
||||
|
||||
最后同步纳入上游治理/文档更新;这不改变已部署的固定 RDM3f647f6 制品。同步后已再次只读核验实际 executable/hash、实例、唯一 ready 绑定、两成员身份、HTTP200/匿名401以及临时凭据全部撤销,结果通过。未因记录工作区的变更重复部署或创建认证材料。
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"instance": "svc-dev-328312de05f9",
|
||||
"runtime": "/home/weichen/projects/Agent-System-Dev/runtime/dev",
|
||||
"target": "3f647f6c547d3f0d5ab65310cc5f3eabf66e654f",
|
||||
"hostProjectId": "prj_8d20020e5b79"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"source": "SELF",
|
||||
"release": "releases/rdm-3f647f6-20261013T155418Z-3842908",
|
||||
"pid": 3896104,
|
||||
"executable": "/home/weichen/projects/Agent-System-Dev/runtime/dev/releases/rdm-3f647f6-20261013T155418Z-3842908/bin/agent-system",
|
||||
"binary_sha256": "d5db3d0ac292d99582ec91938317af356c050ad61e61f42c097e7faf7a731765",
|
||||
"matches_verified_product": true,
|
||||
"instance": "svc-dev-328312de05f9",
|
||||
"old_business_rows_preserved": true,
|
||||
"old_artifact_bytes_preserved": 14,
|
||||
"weichen": {
|
||||
"authenticated_as": "weichen",
|
||||
"project_ids": [
|
||||
"prj_8d20020e5b79"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"authenticated_as": "test",
|
||||
"project_ids": []
|
||||
},
|
||||
"member_create_before_repair_status": 404,
|
||||
"preview": {
|
||||
"hostProjectId": "prj_8d20020e5b79",
|
||||
"name": "genui-e2e",
|
||||
"memberCount": 2,
|
||||
"alreadyBound": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import hashlib,json,os,sqlite3
|
||||
from pathlib import Path
|
||||
import client,prepare
|
||||
W=client.W;R=prepare.R;HOST=prepare.HOST
|
||||
before=json.loads((W/'baseline-private.json').read_text())
|
||||
release=os.readlink(R/'current');assert release.startswith('releases/rdm-3f647f6-')
|
||||
pid=int((R/'svc.pid').read_text());exe=Path(f'/proc/{pid}/exe').resolve();assert exe==(R/'current/bin/agent-system').resolve()
|
||||
ref=Path('/home/weichen/projects/Agent-System-RDM-wt-feature-project-member-access/bin/agent-system-rdm-product')
|
||||
assert prepare.sha(exe)==prepare.sha(ref),'deployed bytes differ from the verified default product'
|
||||
assert json.loads((R/'manifest.json').read_text())['instance_id']==prepare.INSTANCE
|
||||
assert prepare.logical(prepare.DATA)==before['logical'],'old business rows changed before adoption'
|
||||
for a in before['artifacts']:assert prepare.sha(Path(a['content_path']))==a['sha256']
|
||||
result={'source':'SELF','release':release,'pid':pid,'executable':str(exe),'binary_sha256':prepare.sha(exe),'matches_verified_product':True,'instance':prepare.INSTANCE,'old_business_rows_preserved':True,'old_artifact_bytes_preserved':len(before['artifacts'])}
|
||||
for owner in ['weichen','test']:
|
||||
code,me=client.request(owner,'/api/me');assert code==200 and me['owner']==owner, 'authenticated account mismatch'
|
||||
code,projects=client.request(owner,'/api/projects');assert code==200
|
||||
result[owner]={'authenticated_as':me['owner'],'project_ids':[x['id'] for x in projects]}
|
||||
if owner=='weichen':
|
||||
target=next(p for p in projects if p['id']==HOST);assert target['settings']['rdm.state']=='unbound'
|
||||
else:assert HOST not in result[owner]['project_ids']
|
||||
code,_=client.request('test','/api/projects/'+HOST+'/conversations','POST',{'title':'runtime pre-repair admission probe'})
|
||||
assert code in (403,404);result['member_create_before_repair_status']=code
|
||||
preview=client.command('weichen','rdm.project.adopt',{'hostProjectId':HOST})
|
||||
assert preview['hostProjectId']==HOST and preview['name']=='genui-e2e' and preview['memberCount']==2 and not preview['alreadyBound']
|
||||
assert prepare.logical(prepare.DATA)==before['logical'],'preview changed business rows'
|
||||
result['preview']=preview
|
||||
client.save('pre-repair-preview.json',result)
|
||||
print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,103 @@
|
||||
import hashlib,json,os,re,shutil,sqlite3,subprocess,sys,time,urllib.request
|
||||
from collections import Counter
|
||||
from datetime import datetime,timezone
|
||||
from pathlib import Path
|
||||
W=Path('/tmp/project-member-runtime-8788-20261013')
|
||||
ROOT=Path('/home/weichen/projects/Agent-System-Dev')
|
||||
R=ROOT/'runtime/dev'; DATA=R/'data'; SVC=ROOT/'scripts/svc.sh'
|
||||
INSTANCE='svc-dev-328312de05f9'; HOST='prj_8d20020e5b79'
|
||||
def save(name,obj):
|
||||
p=W/name;p.write_text(json.dumps(obj,ensure_ascii=False,indent=2)+'\n');p.chmod(0o600)
|
||||
def sha(p):
|
||||
h=hashlib.sha256()
|
||||
with open(p,'rb') as f:
|
||||
for b in iter(lambda:f.read(1024*1024),b''):h.update(b)
|
||||
return h.hexdigest()
|
||||
def db(path):
|
||||
c=sqlite3.connect('file:'+str(path)+'?mode=ro',uri=True);c.row_factory=sqlite3.Row;return c
|
||||
def digest(row):
|
||||
return hashlib.sha256(json.dumps([{'bytes':v.hex()} if isinstance(v,bytes) else v for v in row],ensure_ascii=False,separators=(',',':')).encode()).hexdigest()
|
||||
def logical(data):
|
||||
result={}
|
||||
sets={'agent-system.db':['users','projects','conversations','messages','artifacts','agent_system_identity'], 'rdm.db':['rdm_projects','rdm_host_project_map','rdm_members','rdm_admins']}
|
||||
for fn,tables in sets.items():
|
||||
with db(data/fn) as c:
|
||||
for t in tables:
|
||||
cols=[x['name'] for x in c.execute('pragma table_info('+t+')')]
|
||||
if cols:result[fn+':'+t]={'columns':cols,'rows':[digest(x) for x in c.execute('select * from '+t)]}
|
||||
return result
|
||||
def inspect():
|
||||
manifest=json.loads((R/'manifest.json').read_text());assert manifest['instance_id']==INSTANCE
|
||||
pid=int((R/'svc.pid').read_text().strip()); exe=Path(f'/proc/{pid}/exe').resolve()
|
||||
assert exe.is_relative_to(R/'releases') and exe.name=='agent-system'
|
||||
args=Path(f'/proc/{pid}/cmdline').read_bytes().decode().split('\0')
|
||||
assert args[args.index('--addr')+1]=='127.0.0.1:8788' and args[args.index('--data')+1]==str(DATA)
|
||||
with db(DATA/'agent-system.db') as c:
|
||||
ident=dict(c.execute('select * from agent_system_identity').fetchone());assert ident['instance_id']==INSTANCE and ident['dbref']=='sqlite'
|
||||
active=c.execute("select count(*) from runs where status not in ('completed','failed','interrupted','cancelled','canceled')").fetchone()[0];assert active==0, 'active runs require drain'
|
||||
hosts=[dict(x) for x in c.execute('select id,name,owner from projects')]
|
||||
target=next(p for p in hosts if p['id']==HOST);assert target['owner']=='weichen' and target['name']=='genui-e2e'
|
||||
artifacts=[dict(x) for x in c.execute('select id,content_path from artifacts')]
|
||||
accounts=[dict(x) for x in c.execute("select owner,must_change_password,auth_version from users where owner in ('weichen','test')")];assert len(accounts)==2 and all(not a['must_change_password'] for a in accounts)
|
||||
with db(DATA/'rdm.db') as c:
|
||||
projects=[dict(x) for x in c.execute('select id,name,created_by from rdm_projects')]
|
||||
maps=[dict(x) for x in c.execute('select * from rdm_host_project_map')]
|
||||
members=[dict(x) for x in c.execute('select project_id,owner,role from rdm_members where project_id=?',(HOST,))]
|
||||
assert not any(m['host_project']==HOST for m in maps)
|
||||
assert not any(p['name'].strip()==target['name'].strip() for p in projects)
|
||||
assert {m['owner'] for m in members}=={'weichen','test'}
|
||||
assert not any(p['id']!=HOST and p['name'].strip()==target['name'].strip() for p in hosts)
|
||||
for a in artifacts:
|
||||
p=Path(a['content_path']);assert p.is_file();a['sha256']=sha(p)
|
||||
result={'pid':pid,'executable':str(exe),'binary_sha256':sha(exe),'release':os.readlink(R/'current'),'identity':ident,'accounts':accounts,'target':target,'target_members':members,'host_projects':hosts,'domain_projects':projects,'bindings':maps,'artifacts':artifacts,'logical':logical(DATA),'captured_utc':datetime.now(timezone.utc).isoformat()}
|
||||
save('baseline-private.json',result)
|
||||
print(json.dumps({'source':'SELF','instance':INSTANCE,'pid':pid,'release':result['release'],'binary_sha256':result['binary_sha256'],'target':target,'target_member_count':len(members),'name_conflict':False,'active_runs':active,'artifact_files_readable':len(artifacts),'business_counts':{k:len(v['rows']) for k,v in result['logical'].items()}},ensure_ascii=False))
|
||||
def backup():
|
||||
before=json.loads((W/'baseline-private.json').read_text())
|
||||
assert sha(Path(f"/proc/{before['pid']}/exe"))==before['binary_sha256']
|
||||
assert os.readlink(R/'current')==before['release']
|
||||
stamp=datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
dest=ROOT/'runtime-backups/dev'/('project-member-'+stamp);dest.mkdir(mode=0o700,parents=True,exist_ok=False)
|
||||
meta=dest/'pre-stop';meta.mkdir(mode=0o700)
|
||||
for label in ('dev','devweb'):
|
||||
q=meta/label;q.mkdir()
|
||||
for n in ('svc.env','serve.env','manifest.json','history.jsonl'):
|
||||
src=ROOT/'runtime'/label/n
|
||||
if src.is_file():shutil.copy2(src,q/n)
|
||||
save('backup-location.json',{'path':str(dest),'before_release':before['release']})
|
||||
print('Cold paired backup start',str(dest),flush=True)
|
||||
stopped=False
|
||||
try:
|
||||
subprocess.run(['bash',str(SVC),'dev','stop'],check=True);stopped=True
|
||||
assert not Path(f"/proc/{before['pid']}").exists(),'old process remains'
|
||||
assert logical(DATA)==before['logical'],'business rows changed since preflight; refresh before backup'
|
||||
for label in ('dev','devweb'):
|
||||
shutil.copytree(ROOT/'runtime'/label,dest/label,symlinks=True)
|
||||
blobs=dest/'artifact-contents';blobs.mkdir(mode=0o700)
|
||||
for a in before['artifacts']:
|
||||
assert re.fullmatch(r'[A-Za-z0-9_-]+',a['id']), 'unsafe artifact identifier'
|
||||
shutil.copyfile(a['content_path'],blobs/a['id']);assert sha(blobs/a['id'])==a['sha256']
|
||||
restored=W/'restore-proof';restored.mkdir(mode=0o700)
|
||||
for fn in ('agent-system.db','rdm.db'):
|
||||
shutil.copy2(dest/'dev/data'/fn,restored/fn)
|
||||
# Cold snapshot is paired; preserve any retained WAL alongside its DB.
|
||||
for suffix in ('-wal','-shm'):
|
||||
src=dest/'dev/data'/(fn+suffix)
|
||||
if src.exists():shutil.copy2(src,restored/(fn+suffix))
|
||||
with db(restored/fn) as c:assert c.execute('pragma integrity_check').fetchone()[0]=='ok'
|
||||
assert logical(restored)==before['logical'],'restored business rows mismatch'
|
||||
inventory={}
|
||||
for p in dest.rglob('*'):
|
||||
if p.is_symlink():inventory[str(p.relative_to(dest))]={'link':os.readlink(p)}
|
||||
elif p.is_file():inventory[str(p.relative_to(dest))]={'sha256':sha(p),'bytes':p.stat().st_size}
|
||||
(dest/'MANIFEST.json').write_text(json.dumps({'instance':INSTANCE,'before_release':before['release'],'files':inventory},indent=2)+'\n');(dest/'MANIFEST.json').chmod(0o600)
|
||||
result={'source':'SELF','backup':str(dest),'instance':INSTANCE,'old_release_retained':before['release'],'runtime_directories':['dev','devweb'],'full_release_count':len(list((dest/'dev/releases').iterdir())),'restored_databases':['agent-system.db','rdm.db'],'integrity_check':'ok','logical_rows_match':True,'artifact_bytes_match':len(before['artifacts']),'manifest_sha256':sha(dest/'MANIFEST.json')}
|
||||
save('backup-proof.json',result);print(json.dumps(result,ensure_ascii=False),flush=True)
|
||||
finally:
|
||||
if stopped:
|
||||
subprocess.run(['bash',str(SVC),'dev','start'],check=True)
|
||||
with urllib.request.urlopen('http://127.0.0.1:8788/',timeout=15) as res:assert res.status==200
|
||||
assert sha((R/'current/bin/agent-system'))==before['binary_sha256']
|
||||
print('Original release resumed after backup; candidate deployment has not started',flush=True)
|
||||
if __name__=='__main__':
|
||||
{'inspect':inspect,'backup':backup}[sys.argv[1]]()
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"reviewer": "/root/runtime_acceptance_review",
|
||||
"parent": "/root",
|
||||
"scope": "independent read-only runtime/data acceptance replay",
|
||||
"independent_of_deployment_and_author_record_writing": true,
|
||||
"author_records_read_after_minimum_replay": true,
|
||||
"http_methods": [
|
||||
"GET"
|
||||
],
|
||||
"sqlite_mode": "ro",
|
||||
"outputs": "/tmp only",
|
||||
"source": "SELF",
|
||||
"source_label_not_upgraded": true,
|
||||
"utc": "2026-10-13T16:24:44.420383+00:00",
|
||||
"receipt": "/tmp/project-member-runtime-independent-replay.json",
|
||||
"comparison_receipt": "/tmp/project-member-runtime-independent-comparison-receipt.json",
|
||||
"browser_claims": "author SELF report checked for identity and scope; UI not independently rerun",
|
||||
"temporary_credential_revocation": "parent responsibility after this review; not performed by reviewer",
|
||||
"replay_script_sha256": "6ecd9e65a9d83f5d9e6764addfa8f6aecf46131f800e7a762aef459bc03617fe",
|
||||
"comparison_script_sha256": "89a7064f9ec8e934dae022230ba3e6fd746300e4213c658ad414a7b4df9b68f0"
|
||||
}
|
||||
+34
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
# 独立运行验收复核:PASS
|
||||
|
||||
审阅者 `/root/runtime_acceptance_review` 未参与部署、修复及作者验收记录编写。先独立重放,再读取作者记录;只使用 HTTP GET、SQLite `mode=ro` 和 `/tmp` 输出。无实质 finding。
|
||||
|
||||
对象:8788 / `svc-dev-328312de05f9`;真实 `/proc/3896104/exe` 为 `rdm-3f647f6-20261013T155418Z-3842908`,SHA256 `d5db3d0ac292d99582ec91938317af356c050ad61e61f42c097e7faf7a731765`;运行参数及打开的文件描述符均指向原 `runtime/dev/data` 两库,manifest 与库身份一致。release 标记 `rdm@3f647f6`;完整源 SHA 的对应关系由已验证产物的二进制 SHA 固定,本复核不重做 CI/标签。
|
||||
|
||||
- weichen/test 的 `/api/me` 均 200,并看到同一 `prj_8d20020e5b79` → `p-aea70b2559c6` ready 绑定;binding 唯一,weichen=owner、test=member,旧 host 两条孤立成员已迁移。
|
||||
- 自己的合成会话/文件 GET 200,文件 SHA256 与 fixture 一致;对方会话/文件 GET 404,列表不泄露对方内容。test 对 U7 会话及工件列表 GET 404,额外重放 3 条原 U7 会话直接 GET 均拒绝;U7 原域 `p-39dd859c3c99` 保持独立。U7 没有工件原件可供直接内容 GET;工件列表拒绝已重放。
|
||||
- 原 7 会话/35 消息/14 工件行摘要完整保留,14 原件字节及冷备份副本字节一致;其他项目成员行摘要完全相同。两个冷备份库 integrity_check 均 ok,业务行与原 baseline 一致,配对 dev/devweb、保留 release 及 manifest 具备;manifest hash 与作者记录一致。
|
||||
- 作者四份 HTTP/预览/收养/备份记录与重放一致。作者 HTTP 当时 9 会话,本重放 12;全部 5 条新增会话均为“成员协作”前缀且零消息,对应 HTTP 2 条、首轮 UI 1 条、最终 UI 2 条,属于已授权新增。当前消息35、工件16。
|
||||
- `acceptance-browser.json` 的 host/domain、两账户、退出切换、隔离/刷新字段及 driver SHA 与当前对象一致,pageerrorCount=0;此项为作者 SELF 报告的身份/范围核对,未独立重跑 UI。
|
||||
|
||||
重放命令:`PYTHONDONTWRITEBYTECODE=1 python3 /tmp/project-member-runtime-independent-replay.py`;对照命令:`PYTHONDONTWRITEBYTECODE=1 python3 /tmp/project-member-runtime-independent-compare.py`。均由 `/home/weichen/projects/Agent-System-Dev-wt-feature-project-member-runtime/scripts/evidence.sh` 在该工作区、显式 RECEIPT 模式执行,退出码均0;工具 source 保持 SELF,独立执行者身份另附 attestation,未将本地收据自行升级为 GATE/REPLAY。
|
||||
|
||||
原始收据:`/tmp/project-member-runtime-independent-replay.json`;作者对照及追加 U7 GET 收据:`/tmp/project-member-runtime-independent-comparison-receipt.json`;身份声明:`/tmp/project-member-runtime-independent-attestation.json`。
|
||||
|
||||
限定:未修改业务/源码/任务卡/索引;未部署、重启、创建、删除、发送消息或调用模型。密码登录按用户决定 NOT_RUN;作者 POST 类权限拒绝和重复收养行为未独立重放。临时 API key 撤销及撤销后401由协调者在本复核结束后执行,尚不属于本报告已验证结果。
|
||||
Reference in New Issue
Block a user