Archive the completed project-member-runtime task card #613

Merged
weichen merged 1 commits from feature/card-close-project-member-runtime-20261018 into main 2026-10-18 09:12:11 +00:00
30 changed files with 2018 additions and 0 deletions
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]]()
@@ -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"
}
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由协调者在本复核结束后执行,尚不属于本报告已验证结果。