@topolo/cli 0.13.11 → 0.13.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -448,3 +448,13 @@ remain unqualified. Native CLI effects therefore refuse with
|
|
|
448
448
|
`native_cli_startup_unqualified` before client/helper/native access. Private MCP
|
|
449
449
|
startup retains its existing refusal. Version 1 plans stay readable and do not
|
|
450
450
|
silently become version 2 CLI plans.
|
|
451
|
+
|
|
452
|
+
### Preserving private pack runtime configuration
|
|
453
|
+
|
|
454
|
+
`topolo agent-packs --store /owned/AgentPacks runtime-adoption plan --manifest /review/adoption.json` produces a nonwriting configuration plan. Review its physical targets and digest, then use `runtime-adoption apply --manifest /review/adoption.json --plan-digest <sha256> --confirm` for one configuration-only attempt.
|
|
455
|
+
|
|
456
|
+
The closed owner manifest binds the actual consumer set and effective local Topolo transport origins, physical file preimages, shared aliases, private profile metadata, immutable CLI 0.13.11/MCP 0.14.26/SDK 0.23.21 targets and the separately pinned successor Python adapter. It must include every server in each physical config as either a targeted transport or an explicitly untouched server. Empty or incomplete manifests refuse. The command never installs skills, rebinds a home, changes existing instructions/permissions, starts a consumer or probes Auth.
|
|
457
|
+
|
|
458
|
+
A successful apply means configuration is prepared. It does not mean existing processes reloaded it, the private account is healthy, permissions were granted or either native harness completed a task. Starting or refreshing consumers is a separate owner-authorized step and can contact private Auth and renew the selected private profile. Public credential-store repair is not a blanket prerequisite for independent private profiles. See [the manifest and effect boundaries](agent-packs/README.md#private-runtime-adoption).
|
|
459
|
+
|
|
460
|
+
The protocol SDK pin addresses [GHSA-6qxp-vccf-f47h](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-6qxp-vccf-f47h). The CLI doctor's protocol transport is stdio. Separate HTTP OAuth consumers still need appropriate issuer binding for legacy credentials and bundled providers; this source update neither inspects nor migrates credentials.
|
package/agent-packs/README.md
CHANGED
|
@@ -699,3 +699,26 @@ contracts are unavailable. Effects stay closed with
|
|
|
699
699
|
`native_cli_tool_result_unqualified`. Input references, hashes or JSON flags do
|
|
700
700
|
not grant admission. Private MCP is a separate unsupported transport and is never
|
|
701
701
|
a fallback. Source fixtures do not activate either native harness.
|
|
702
|
+
|
|
703
|
+
## Private runtime adoption
|
|
704
|
+
|
|
705
|
+
The public CLI forwards `agent-packs runtime-adoption plan|apply` to the bundled successor adapter. `plan` performs no writes. `apply` requires the exact reviewed `--plan-digest` and `--confirm`; it never calls `prepare`, `bind`, `reconnect`, skill installation or a native/Auth client. The selected immutable public CLI/MCP cohort is separate from the successor Python adapter source. Do not edit an installed immutable runtime to add the successor hooks.
|
|
706
|
+
|
|
707
|
+
The manifest is a local owner-reviewed configuration scope, not native provenance, a permission grant or a health certificate. Its exact top-level fields are:
|
|
708
|
+
|
|
709
|
+
- `schemaVersion: 1`, a fresh UUID `operationId`, and the absolute physical `library`.
|
|
710
|
+
- `ownerGeneration: {path, sha256}`: the owner's current bounded nonsecret effective-transport/generation record, inside that library. The owner must establish its native currency separately.
|
|
711
|
+
- `consumers`: the complete unique existing native IDs in scope. Target rows must account for every ID; no new native IDs are created.
|
|
712
|
+
- `profile: {path, publicPath, metadata, environment, contextKey, principalId}`: absolute private and public credential-file references, explicit organization context, and metadata only. Files must be physically distinct; the private profile must be owned, single-link, regular and mode 0600. Neither profile's contents are read or hashed. A renewal that changes private file metadata invalidates the descriptor and requires owner reconciliation.
|
|
713
|
+
- `runtime: {node, cli, mcp, cohort, pythonSource, adapterFiles, osEnvironment}`. Each executable/entry and each `cohort` value is `{path, sha256}`. Cohort keys `cli`, `mcp`, `sdk` pin their package JSON; required target versions are 0.13.11, 0.14.26 and 0.23.21. CLI and MCP entries must be their pinned package's `dist/index.js`. `pythonSource` is the physical successor source directory; `adapterFiles` pins its `topolo_agent_pack/{cli,live,topolo}.py`. Verify the complete installed cohort independently before admitting its pins. `osEnvironment` permits only HOME/PATH and optional USER/LOGNAME/LANG/LC_ALL/TMPDIR, with no credential, service-origin, Node or installation overrides.
|
|
714
|
+
- `targets`: one row per unique physical configuration file, with `{path, before, aliases, format, rootKey, consumers, transports, untouchedServers}`. Supported formats are JSON `mcpServers` and simple TOML `mcp_servers` tables. Each transport is `{name, origin, consumers}` and must already exist as a local command; URL, inherited `env_vars`, installation-mode, ambiguous names or unsupported TOML layouts refuse. `origin` is the exact physical file. The complete physical server map must equal the targeted names plus `untouchedServers`. The owner must independently bind all effective native layers; a zero-table query or a guessed server name is insufficient.
|
|
715
|
+
- `launcher: {path, before}`: exactly the owned generated `launchers/topolo-agent-pack-backend`; its interpreter, owner and mode are preserved.
|
|
716
|
+
- `preserve`: nonsecret `{path, sha256}` references to existing roster, model/effort, permission, charter/context, routing and lifecycle records. Include every protected fixed and dynamic record in the approved scope. These may not overlap destinations. No transcript or credential contents belong in a manifest.
|
|
717
|
+
|
|
718
|
+
A physical `before` snapshot contains `device`, `inode`, `uid`, numeric permission `mode`, `bytes`, `mtimeNs`, `ctimeNs`, `parents` (absolute path/device/inode triples), and `sha256`. Profile `metadata` uses the same shape without `sha256`. Each alias is `{path, linkTarget, metadata}`, where `metadata` is the symlink's device/inode/uid/full mode/size/mtimeNs/ctimeNs tuple. The plan emits actual beforeimages for review; effect admission must independently establish the owner, complete source set, physical sharing and current generation. It cannot silently authorize another consumer sharing a file.
|
|
719
|
+
|
|
720
|
+
Only existing owned physical destinations inside the library can change. Shared desktop/backend config symlinks remain intact and their common file changes once. The generated private descriptor and isolated Python MCP launcher are operation-specific. The isolated MCP launcher validates successor bytes before import, then current profile/generation/preservation pins before replacing itself with the explicit Node/MCP pair and closed private environment. This retains the native host's PID, stdio and cancellation custody. Existing host agent names are retained as a closed wrapper argument. Managed default helper clients use the exact Node/CLI pair regardless of PATH. Unmanaged clients retain their ordinary behavior. No parent credential/service-origin/installation/Node environment is copied into those Topolo children. An intentional installation credential mode needs its own separately explicit contract.
|
|
721
|
+
|
|
722
|
+
Apply reserves `launchers/adoption-<UUID>.json` exclusively. The journal starts in `may-have-config-effects`, records each verified replacement, and becomes `configuration-prepared-native-adoption-unverified` only after all readbacks. Every write rechecks generation, source, profile metadata, preservation pins and destination custody. An interrupted or conflicting apply retains the journal and any uncertain temporary file; it never automatically replays or rolls back. Reconcile exact effects with the original owner.
|
|
723
|
+
|
|
724
|
+
No apply result proves native reload, identity, current RBAC, model/effort, loaded context, original-intake permission routing or a native tool result. Hosts may react to configuration changes; admit the actual affected owner/generation and normal request settlement before applying in a live host. Later ordinary startup can renew the private credential and load identity. A previous 429/403 or scoped recovery hold remains separately applicable and elapsed time grants no retry. Actual Codex and Claude same-query context, permission, nonce result, cancellation/settlement and own retirement require independent native evidence.
|
|
@@ -290,6 +290,9 @@ def parser():
|
|
|
290
290
|
p.add_argument('--version',action='version',version=__version__)
|
|
291
291
|
p.add_argument('--store',default=os.environ.get('CODEX_PACK_HOME',str(pathlib.Path.home()/'AgentPacks')))
|
|
292
292
|
sub=p.add_subparsers(dest='command',required=True)
|
|
293
|
+
q=sub.add_parser('runtime-adoption',help='Configuration-only private runtime plan/apply; no consumer startup or authentication')
|
|
294
|
+
q.add_argument('operation',choices=['plan','apply']);q.add_argument('--manifest',required=True)
|
|
295
|
+
q.add_argument('--plan-digest');q.add_argument('--confirm',action='store_true')
|
|
293
296
|
def selection(q):
|
|
294
297
|
q.add_argument('--set',dest='sets',action='append');q.add_argument('--agent',dest='agents',action='append')
|
|
295
298
|
def activation(q):
|
|
@@ -374,7 +377,7 @@ def main(argv=None):
|
|
|
374
377
|
elif command_args and command_args[0].startswith('--store='):command_args=command_args[1:]
|
|
375
378
|
lane_command=bool(command_args and command_args[0]=='lane')
|
|
376
379
|
cloud_command=bool(command_args and command_args[0] in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'})
|
|
377
|
-
if harness == 'claude' and not policy_command and not cloud_command and not lane_command:
|
|
380
|
+
if harness == 'claude' and not policy_command and not cloud_command and not lane_command and not (command_args and command_args[0]=='runtime-adoption'):
|
|
378
381
|
from . import claude
|
|
379
382
|
return claude.main(argv)
|
|
380
383
|
if harness not in {'codex', 'claude'}:
|
|
@@ -383,6 +386,11 @@ def main(argv=None):
|
|
|
383
386
|
args=parser().parse_args(argv)
|
|
384
387
|
args.harness=harness
|
|
385
388
|
try:
|
|
389
|
+
if args.command=='runtime-adoption':
|
|
390
|
+
if harness!='codex':
|
|
391
|
+
raise PackError('Runtime adoption is one explicit cross-host owner manifest, not a harness override')
|
|
392
|
+
emit(live.private_runtime_adoption(args.store,args.manifest,args.operation,args.plan_digest,args.confirm))
|
|
393
|
+
return 0
|
|
386
394
|
lib=Library(args.store)
|
|
387
395
|
if args.command in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'}:
|
|
388
396
|
from . import topolo
|
|
@@ -1990,6 +1990,357 @@ def bind(lib,runtime):
|
|
|
1990
1990
|
dest.symlink_to(expected)
|
|
1991
1991
|
|
|
1992
1992
|
|
|
1993
|
+
def adoption_require(value,message):
|
|
1994
|
+
if not value:raise PackError('Runtime adoption refused: '+message)
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
def adoption_keys(value,keys):
|
|
1998
|
+
adoption_require(isinstance(value,dict) and set(value)==set(keys),'closed fields required: '+','.join(keys))
|
|
1999
|
+
|
|
2000
|
+
|
|
2001
|
+
def adoption_json(raw):
|
|
2002
|
+
def unique(pairs):
|
|
2003
|
+
adoption_require(len(pairs)==len({key for key,_ in pairs}),'duplicate JSON fields')
|
|
2004
|
+
return dict(pairs)
|
|
2005
|
+
return json.loads(raw,object_pairs_hook=unique)
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
def adoption_snapshot(path,read=True,missing=False,profile=None):
|
|
2009
|
+
"""Single-file custody. Credential profiles use read=False exclusively."""
|
|
2010
|
+
import stat
|
|
2011
|
+
if read and profile is not None:adoption_read_guard(profile,[path])
|
|
2012
|
+
path=pathlib.Path(path)
|
|
2013
|
+
adoption_require(path.is_absolute() and path==path.resolve(),'absolute physical path required')
|
|
2014
|
+
parents=[]
|
|
2015
|
+
for parent in path.parents:
|
|
2016
|
+
st=parent.lstat()
|
|
2017
|
+
adoption_require(stat.S_ISDIR(st.st_mode) and st.st_uid in {0,os.getuid()},'unowned parent')
|
|
2018
|
+
parents.append([str(parent),st.st_dev,st.st_ino])
|
|
2019
|
+
try:st=path.lstat()
|
|
2020
|
+
except FileNotFoundError:
|
|
2021
|
+
adoption_require(missing,'required file absent')
|
|
2022
|
+
return None,None
|
|
2023
|
+
adoption_require(stat.S_ISREG(st.st_mode) and st.st_nlink==1 and st.st_uid==os.getuid(),'owned single regular file required')
|
|
2024
|
+
value={'device':st.st_dev,'inode':st.st_ino,'uid':st.st_uid,'mode':st.st_mode&0o777,
|
|
2025
|
+
'bytes':st.st_size,'mtimeNs':st.st_mtime_ns,'ctimeNs':st.st_ctime_ns,'parents':parents}
|
|
2026
|
+
raw=None
|
|
2027
|
+
if read:
|
|
2028
|
+
adoption_require(st.st_size<=128*1024*1024,'file exceeds limit')
|
|
2029
|
+
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW)
|
|
2030
|
+
with os.fdopen(fd,'rb') as stream:
|
|
2031
|
+
opened=os.fstat(stream.fileno())
|
|
2032
|
+
adoption_require((opened.st_dev,opened.st_ino,opened.st_uid,opened.st_mode,opened.st_nlink,opened.st_size,opened.st_mtime_ns,opened.st_ctime_ns)==
|
|
2033
|
+
(st.st_dev,st.st_ino,st.st_uid,st.st_mode,st.st_nlink,st.st_size,st.st_mtime_ns,st.st_ctime_ns),'file changed before content read')
|
|
2034
|
+
raw=stream.read()
|
|
2035
|
+
after=path.lstat()
|
|
2036
|
+
adoption_require((after.st_dev,after.st_ino,after.st_size,after.st_mtime_ns,after.st_ctime_ns)==
|
|
2037
|
+
(st.st_dev,st.st_ino,st.st_size,st.st_mtime_ns,st.st_ctime_ns),'file changed while reading')
|
|
2038
|
+
value['sha256']=hashlib.sha256(raw).hexdigest()
|
|
2039
|
+
return value,raw
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def adoption_pin(value,profile=None):
|
|
2043
|
+
adoption_keys(value,['path','sha256'])
|
|
2044
|
+
snapshot,raw=adoption_snapshot(value['path'],profile=profile)
|
|
2045
|
+
adoption_require(snapshot['sha256']==value['sha256'],'source or preservation pin changed')
|
|
2046
|
+
return raw
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def adoption_profile(profile):
|
|
2050
|
+
adoption_keys(profile,['path','publicPath','metadata','environment','contextKey','principalId'])
|
|
2051
|
+
adoption_require(profile['environment'] in {'production','staging','development'} and
|
|
2052
|
+
isinstance(profile['contextKey'],str) and re.fullmatch(r'org:[A-Za-z0-9_-]+',profile['contextKey']) and
|
|
2053
|
+
isinstance(profile['principalId'],str) and re.fullmatch(r'[A-Za-z0-9_-]+',profile['principalId']), 'explicit private identity tuple required')
|
|
2054
|
+
current,_=adoption_snapshot(profile['path'],read=False)
|
|
2055
|
+
adoption_require(current==profile['metadata'] and current['mode']==0o600,'private profile metadata changed')
|
|
2056
|
+
public,_=adoption_snapshot(profile['publicPath'],read=False,missing=True)
|
|
2057
|
+
adoption_require(profile['path']!=profile['publicPath'] and (public is None or
|
|
2058
|
+
(public['device'],public['inode'])!=(current['device'],current['inode'])),'private/public profile alias')
|
|
2059
|
+
|
|
2060
|
+
|
|
2061
|
+
def adoption_read_guard(profile,paths):
|
|
2062
|
+
"""Classify every declared readable input before any non-manifest content read."""
|
|
2063
|
+
adoption_profile(profile)
|
|
2064
|
+
protected=[(str(pathlib.Path(profile[key]).resolve()),adoption_snapshot(profile[key],read=False,missing=True)[0])
|
|
2065
|
+
for key in ['path','publicPath']]
|
|
2066
|
+
for path in paths:
|
|
2067
|
+
current,_=adoption_snapshot(path,read=False,missing=True)
|
|
2068
|
+
adoption_require(all(str(pathlib.Path(path).resolve())!=name and
|
|
2069
|
+
(current is None or saved is None or (current['device'],current['inode'])!=(saved['device'],saved['inode']))
|
|
2070
|
+
for name,saved in protected),'readable input aliases protected profile; no contents read')
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
def adoption_runtime_paths(runtime):
|
|
2074
|
+
return [runtime[key]['path'] for key in ['node','cli','mcp']]+[pin['path'] for pin in runtime['cohort'].values()]+[pin['path'] for pin in runtime['adapterFiles']]
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
def adoption_read_inputs(value):
|
|
2078
|
+
paths=[value['ownerGeneration']['path'],*adoption_runtime_paths(value['runtime']),*[pin['path'] for pin in value['preserve']],
|
|
2079
|
+
*[target['path'] for target in value.get('targets',[])]]
|
|
2080
|
+
if 'launcher' in value:paths.append(value['launcher']['path'])
|
|
2081
|
+
adoption_read_guard(value['profile'],paths)
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def adoption_runtime(runtime,profile):
|
|
2085
|
+
adoption_keys(runtime,['node','cli','mcp','cohort','pythonSource','adapterFiles','osEnvironment'])
|
|
2086
|
+
adoption_read_guard(profile,adoption_runtime_paths(runtime))
|
|
2087
|
+
for key in ['node','cli','mcp']:adoption_pin(runtime[key],profile)
|
|
2088
|
+
adoption_keys(runtime['cohort'],['cli','mcp','sdk'])
|
|
2089
|
+
for key,name,version in [('cli','@topolo/cli','0.13.11'),('mcp','@topolo/mcp','0.14.26'),('sdk','@topolo/sdk','0.23.21')]:
|
|
2090
|
+
package=json.loads(adoption_pin(runtime['cohort'][key],profile))
|
|
2091
|
+
adoption_require(package.get('name')==name and package.get('version')==version,'immutable target cohort differs')
|
|
2092
|
+
if key!='sdk':
|
|
2093
|
+
adoption_require(runtime[key]['path']==str(pathlib.Path(runtime['cohort'][key]['path']).parent/'dist/index.js') and
|
|
2094
|
+
package.get('dependencies',{}).get('@topolo/sdk')=='0.23.21','entry/dependency cohort differs')
|
|
2095
|
+
source=pathlib.Path(runtime['pythonSource'])
|
|
2096
|
+
adoption_require(source.is_absolute() and source==source.resolve(),'physical adapter source required')
|
|
2097
|
+
adoption_require(isinstance(runtime['adapterFiles'],list) and len(runtime['adapterFiles'])==3 and
|
|
2098
|
+
{p['path'] for p in runtime['adapterFiles']}=={str(source/'topolo_agent_pack'/n) for n in ['cli.py','live.py','topolo.py']},'exact successor adapter source pins required')
|
|
2099
|
+
for value in runtime['adapterFiles']:adoption_pin(value,profile)
|
|
2100
|
+
env=runtime['osEnvironment']
|
|
2101
|
+
adoption_require(isinstance(env,dict) and {'HOME','PATH'}<=set(env)<=
|
|
2102
|
+
{'HOME','PATH','USER','LOGNAME','LANG','LC_ALL','TMPDIR'} and
|
|
2103
|
+
all(isinstance(v,str) and v and len(v)<=4096 and not any(ord(c)<32 for c in v) for v in env.values()),'closed nonsecret OS environment required')
|
|
2104
|
+
env=dict(env)
|
|
2105
|
+
env.update(TOPOLO_CONFIG_PATH=profile['path'],TOPOLO_ENV=profile['environment'],TOPOLO_CONTEXT=profile['contextKey'],
|
|
2106
|
+
TOPOLO_API_KEY='',TOPOLO_ACCESS_TOKEN='')
|
|
2107
|
+
prefix='TOPOLO_'+profile['environment'].upper()
|
|
2108
|
+
env.update({prefix+'_API_KEY':'',prefix+'_ACCESS_TOKEN':''})
|
|
2109
|
+
return env
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
def adoption_alias(alias,target):
|
|
2113
|
+
adoption_keys(alias,['path','linkTarget','metadata']);p=pathlib.Path(alias['path']);st=p.lstat()
|
|
2114
|
+
value=[st.st_dev,st.st_ino,st.st_uid,st.st_mode,st.st_size,st.st_mtime_ns,st.st_ctime_ns]
|
|
2115
|
+
adoption_require(p.is_symlink() and os.readlink(p)==alias['linkTarget'] and str(p.resolve())==target and
|
|
2116
|
+
st.st_uid==os.getuid() and value==alias['metadata'],'shared alias changed')
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
def adoption_config(raw,target,mcp_launcher):
|
|
2120
|
+
"""Registration-only transform; never installs skills or rewrites context."""
|
|
2121
|
+
import tomllib
|
|
2122
|
+
adoption_require(target['format'] in {'json','toml'} and target['rootKey'] in {'mcp_servers','mcpServers'},'unsupported configuration format')
|
|
2123
|
+
config=adoption_json(raw) if target['format']=='json' else tomllib.loads(raw.decode())
|
|
2124
|
+
servers=config.get(target['rootKey'])
|
|
2125
|
+
adoption_require(isinstance(servers,dict),'existing server map required')
|
|
2126
|
+
names=[v['name'] for v in target['transports']]
|
|
2127
|
+
adoption_require(isinstance(target['untouchedServers'],list) and all(isinstance(n,str) for n in target['untouchedServers']) and
|
|
2128
|
+
len(set(target['untouchedServers']))==len(target['untouchedServers']) and
|
|
2129
|
+
not set(names)&set(target['untouchedServers']) and set(servers)==set(names)|set(target['untouchedServers']), 'incomplete physical server map')
|
|
2130
|
+
for transport in target['transports']:
|
|
2131
|
+
name=transport['name']
|
|
2132
|
+
adoption_require(isinstance(name,str) and re.fullmatch(r'[A-Za-z0-9_-]+',name) and isinstance(servers.get(name),dict),'unambiguous existing local transport required')
|
|
2133
|
+
entry=servers[name]
|
|
2134
|
+
adoption_require(isinstance(entry.get('command'),str) and 'url' not in entry and 'env_vars' not in entry,'local explicit transport required')
|
|
2135
|
+
adoption_require(not entry.get('env',{}).get('TOPOLO_AGENT_INSTALLATION_ID'),'installation mode requires a separate explicit adoption')
|
|
2136
|
+
agent=entry.get('env',{}).get('TOPOLO_AGENT_NAME','')
|
|
2137
|
+
adoption_require(isinstance(agent,str) and (not agent or re.fullmatch(r'[A-Za-z0-9_-]+',agent)),'agent name differs')
|
|
2138
|
+
entry['command']=mcp_launcher;entry['args']=[agent] if agent else [];entry['env']={'TOPOLO_AGENT_NAME':agent} if agent else {}
|
|
2139
|
+
if target['format']=='json':return (json.dumps(config,indent=2)+'\n').encode()
|
|
2140
|
+
text=raw.decode()
|
|
2141
|
+
for transport in target['transports']:
|
|
2142
|
+
name=transport['name'];entry=servers[name]
|
|
2143
|
+
# Qualified simple tables only. Quoted/dotted/array-table variants refuse;
|
|
2144
|
+
# they cannot silently coexist with a new duplicate registration.
|
|
2145
|
+
section='[mcp_servers.'+name+']';environment='[mcp_servers.'+name+'.env]'
|
|
2146
|
+
adoption_require(target['rootKey']=='mcp_servers' and text.splitlines().count(section)==1 and
|
|
2147
|
+
text.splitlines().count(environment)<=1,'unsupported TOML registration layout')
|
|
2148
|
+
kept=[];skipping=False
|
|
2149
|
+
for line in text.splitlines():
|
|
2150
|
+
if line.strip() in {section,environment}:skipping=True
|
|
2151
|
+
elif line.lstrip().startswith('['):
|
|
2152
|
+
adoption_require(not line.strip().startswith('[mcp_servers.'+name+'.'),'unsupported nested transport table')
|
|
2153
|
+
skipping=False
|
|
2154
|
+
if not skipping:kept.append(line)
|
|
2155
|
+
values=[]
|
|
2156
|
+
for key,value in entry.items():
|
|
2157
|
+
if key=='env':continue
|
|
2158
|
+
adoption_require(re.fullmatch(r'[A-Za-z0-9_-]+',key) and
|
|
2159
|
+
(isinstance(value,(str,bool,int)) or isinstance(value,list) and all(isinstance(v,str) for v in value)), 'unsupported TOML transport value')
|
|
2160
|
+
values.append(key+' = '+json.dumps(value))
|
|
2161
|
+
text='\n'.join(kept).rstrip()+'\n\n'+section+'\n'+'\n'.join(values)+'\n\n'+environment+'\n'+\
|
|
2162
|
+
'\n'.join(key+' = '+json.dumps(value) for key,value in entry['env'].items())+'\n'
|
|
2163
|
+
adoption_require(tomllib.loads(text)==config,'TOML semantic preservation differs')
|
|
2164
|
+
return text.encode()
|
|
2165
|
+
|
|
2166
|
+
|
|
2167
|
+
def adoption_bootstrap(source,descriptor,runtime):
|
|
2168
|
+
"""Check successor bytes before importing them, including the verifier itself."""
|
|
2169
|
+
pins=[(p['path'],p['sha256']) for p in runtime['runtime']['adapterFiles']]
|
|
2170
|
+
pins.append((str(descriptor),hashlib.sha256(canonical(runtime)+b'\n').hexdigest()))
|
|
2171
|
+
return 'import hashlib,os,pathlib,stat,sys\nsys.dont_write_bytecode=True\nfor name,digest in '+repr(pins)+':\n'+\
|
|
2172
|
+
' p=pathlib.Path(name);s=p.lstat()\n'+\
|
|
2173
|
+
' if p!=p.resolve() or not stat.S_ISREG(s.st_mode) or s.st_uid!=os.getuid() or s.st_nlink!=1:raise SystemExit("Runtime source custody changed")\n'+\
|
|
2174
|
+
' with p.open("rb") as f: actual=hashlib.file_digest(f,"sha256").hexdigest()\n'+\
|
|
2175
|
+
' if actual!=digest:raise SystemExit("Runtime source changed before import")\n'+\
|
|
2176
|
+
'sys.path.insert(0,'+repr(source)+')\n'
|
|
2177
|
+
|
|
2178
|
+
|
|
2179
|
+
def adoption_known_launcher(raw,root,profile=None):
|
|
2180
|
+
import ast
|
|
2181
|
+
lines=raw.decode().splitlines();tail=['from topolo_agent_pack.live import proxy_main','raise SystemExit(proxy_main())']
|
|
2182
|
+
if len(lines)==5 and lines[1]=='import sys' and lines[2].startswith('sys.path.insert(0,'):
|
|
2183
|
+
return lines[0].startswith('#!/') and lines[-2:]==tail
|
|
2184
|
+
assignments=[line for line in lines if line.startswith('os.environ["TOPOLO_AGENT_PACK_RUNTIME"]=')]
|
|
2185
|
+
if len(assignments)!=1:return False
|
|
2186
|
+
descriptor=pathlib.Path(ast.literal_eval(assignments[0].split('=',1)[1]))
|
|
2187
|
+
adoption_require(descriptor.parent==root/'launchers' and descriptor.name.startswith('private-runtime-'),'unknown managed launcher descriptor')
|
|
2188
|
+
_,value=adoption_snapshot(descriptor,profile=profile);runtime=json.loads(value)
|
|
2189
|
+
expected=lines[0]+'\n'+adoption_bootstrap(runtime['runtime']['pythonSource'],descriptor,runtime)+assignments[0]+'\n'+'\n'.join(tail)+'\n'
|
|
2190
|
+
return raw==expected.encode()
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
def adoption_preflight(store,manifest):
|
|
2194
|
+
adoption_keys(manifest,['schemaVersion','operationId','library','ownerGeneration','consumers','profile','runtime','targets','launcher','preserve'])
|
|
2195
|
+
root=pathlib.Path(store)
|
|
2196
|
+
adoption_require(manifest['schemaVersion']==1 and type(manifest['schemaVersion']) is int and
|
|
2197
|
+
isinstance(manifest['operationId'],str) and re.fullmatch(r'[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}',manifest['operationId']) and
|
|
2198
|
+
root.is_absolute() and str(root)==manifest['library'] and root==root.resolve() and root.stat().st_uid==os.getuid(),'owned library/operation required')
|
|
2199
|
+
def inside(path):
|
|
2200
|
+
adoption_require(pathlib.Path(path).is_relative_to(root),'destination outside owned library')
|
|
2201
|
+
adoption_read_inputs(manifest)
|
|
2202
|
+
inside(manifest['ownerGeneration']['path']);adoption_pin(manifest['ownerGeneration'],manifest['profile'])
|
|
2203
|
+
consumers=manifest['consumers']
|
|
2204
|
+
adoption_require(isinstance(consumers,list) and 1<=len(consumers)<=256 and all(isinstance(c,str) and c for c in consumers) and len(set(consumers))==len(consumers),'closed unique consumer set required')
|
|
2205
|
+
adoption_profile(manifest['profile']);adoption_runtime(manifest['runtime'],manifest['profile'])
|
|
2206
|
+
descriptor=root/'launchers'/('private-runtime-'+manifest['operationId']+'.json')
|
|
2207
|
+
mcp_launcher=root/'launchers'/('private-mcp-'+manifest['operationId'])
|
|
2208
|
+
adoption_require(isinstance(manifest['preserve'],list) and 1<=len(manifest['preserve'])<=2048,'preservation pins required')
|
|
2209
|
+
for value in manifest['preserve']:inside(value['path']);adoption_pin(value,manifest['profile'])
|
|
2210
|
+
targets=manifest['targets'];adoption_require(isinstance(targets,list) and 1<=len(targets)<=256,'nonempty effective transport targets required')
|
|
2211
|
+
writes=[];seen=set();pairs=set()
|
|
2212
|
+
for target in targets:
|
|
2213
|
+
adoption_keys(target,['path','before','aliases','format','rootKey','consumers','transports','untouchedServers'])
|
|
2214
|
+
inside(target['path']);before,raw=adoption_snapshot(target['path'],profile=manifest['profile'])
|
|
2215
|
+
adoption_require(before==target['before'] and target['path'] not in seen,'target preimage changed or duplicated')
|
|
2216
|
+
seen.add(target['path'])
|
|
2217
|
+
adoption_require(isinstance(target['consumers'],list) and target['consumers'] and set(target['consumers'])<=set(consumers),'out-of-scope shared consumer')
|
|
2218
|
+
for alias in target['aliases']:
|
|
2219
|
+
inside(alias['path']);adoption_alias(alias,target['path'])
|
|
2220
|
+
adoption_require(isinstance(target['transports'],list) and target['transports'],'effective transport rows required')
|
|
2221
|
+
for transport in target['transports']:
|
|
2222
|
+
adoption_keys(transport,['name','origin','consumers'])
|
|
2223
|
+
pair=(target['path'],transport['name'])
|
|
2224
|
+
adoption_require(pair not in pairs and transport['origin']==target['path'] and isinstance(transport['consumers'],list) and
|
|
2225
|
+
transport['consumers'] and set(transport['consumers'])<=set(target['consumers']),'transport origin/consumer/precedence conflict')
|
|
2226
|
+
pairs.add(pair)
|
|
2227
|
+
writes.append((target['path'],before,adoption_config(raw,target,str(mcp_launcher))))
|
|
2228
|
+
adoption_require(set(consumers)=={c for t in targets for v in t['transports'] for c in v['consumers']},'incomplete effective consumer manifest')
|
|
2229
|
+
launcher=manifest['launcher'];adoption_keys(launcher,['path','before'])
|
|
2230
|
+
adoption_require(launcher['path']==str(root/'launchers/topolo-agent-pack-backend') and launcher['path'] not in seen,'exact owned launcher required')
|
|
2231
|
+
before,raw=adoption_snapshot(launcher['path'],profile=manifest['profile']);adoption_require(before==launcher['before'],'launcher preimage changed')
|
|
2232
|
+
lines=raw.decode().splitlines()
|
|
2233
|
+
adoption_require(adoption_known_launcher(raw,root,manifest['profile']),'unrecognized launcher source')
|
|
2234
|
+
current,_=adoption_snapshot(descriptor,missing=True,profile=manifest['profile']);adoption_require(current is None,'runtime descriptor already exists; reconcile without replay')
|
|
2235
|
+
current,_=adoption_snapshot(mcp_launcher,missing=True,profile=manifest['profile']);adoption_require(current is None,'MCP launcher already exists; reconcile without replay')
|
|
2236
|
+
runtime={'schemaVersion':1,'library':str(root),'ownerGeneration':manifest['ownerGeneration'],'profile':manifest['profile'],
|
|
2237
|
+
'runtime':manifest['runtime'],'preserve':manifest['preserve']}
|
|
2238
|
+
writes.append((str(descriptor),None,canonical(runtime)+b'\n'))
|
|
2239
|
+
source=manifest['runtime']['pythonSource']
|
|
2240
|
+
adoption_require(' ' not in lines[0],'MCP wrapper requires an absolute Python shebang without flags')
|
|
2241
|
+
mcp_code=lines[0]+' -I\n'+adoption_bootstrap(source,descriptor,runtime)+'from topolo_agent_pack.topolo import managed_mcp_main\nraise SystemExit(managed_mcp_main('+repr(str(descriptor))+',sys.argv[1:]))\n'
|
|
2242
|
+
writes.append((str(mcp_launcher),None,mcp_code.encode()))
|
|
2243
|
+
code=lines[0]+'\n'+adoption_bootstrap(source,descriptor,runtime)+'os.environ["TOPOLO_AGENT_PACK_RUNTIME"]='+repr(str(descriptor))+'\nfrom topolo_agent_pack.live import proxy_main\nraise SystemExit(proxy_main())\n'
|
|
2244
|
+
writes.append((launcher['path'],before,code.encode()))
|
|
2245
|
+
protected={manifest['profile']['path'],manifest['profile']['publicPath'],manifest['ownerGeneration']['path'],*[p['path'] for p in manifest['preserve']]}
|
|
2246
|
+
protected.update(pin['path'] for pin in [manifest['runtime'][k] for k in ['node','cli','mcp']]+
|
|
2247
|
+
list(manifest['runtime']['cohort'].values())+manifest['runtime']['adapterFiles'])
|
|
2248
|
+
adoption_require(not protected&{p for p,_,_ in writes},'destination overlaps protected profile/generation/context')
|
|
2249
|
+
return writes
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
def private_runtime_adoption(store,manifest_path,operation,plan_digest=None,confirm=False):
|
|
2253
|
+
try:return _private_runtime_adoption(store,manifest_path,operation,plan_digest,confirm)
|
|
2254
|
+
except (KeyError,TypeError,AttributeError,SyntaxError,UnicodeError) as error:
|
|
2255
|
+
raise PackError('Runtime adoption refused: malformed typed manifest or owned launcher') from error
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
def _private_runtime_adoption(store,manifest_path,operation,plan_digest=None,confirm=False):
|
|
2259
|
+
"""Owner-reviewed configuration transaction, NOT native/Auth admission.
|
|
2260
|
+
|
|
2261
|
+
The closed manifest is the owner's effect scope. It does not certify a native
|
|
2262
|
+
inventory, current grants, health, startup or a successful tool result.
|
|
2263
|
+
"""
|
|
2264
|
+
adoption_require(operation in {'plan','apply'} and (operation=='apply' and confirm is True and
|
|
2265
|
+
isinstance(plan_digest,str) and re.fullmatch(r'[a-f0-9]{64}',plan_digest) or
|
|
2266
|
+
operation=='plan' and plan_digest is None and confirm is False),'closed plan/confirmed apply required')
|
|
2267
|
+
manifest_before,raw=adoption_snapshot(manifest_path);adoption_require(len(raw)<=2*1024*1024,'manifest exceeds limit')
|
|
2268
|
+
manifest=adoption_json(raw);writes=adoption_preflight(store,manifest)
|
|
2269
|
+
journal=pathlib.Path(store)/'launchers'/('adoption-'+manifest['operationId']+'.json')
|
|
2270
|
+
adoption_read_guard(manifest['profile'],[journal,*[p+'.'+manifest['operationId']+'.tmp' for p,_,_ in writes]])
|
|
2271
|
+
adoption_require(not journal.exists() and not journal.is_symlink(),'operation already reserved; reconcile without replay')
|
|
2272
|
+
plan={'schemaVersion':1,'operationId':manifest['operationId'],'manifestDigest':hashlib.sha256(raw).hexdigest(),
|
|
2273
|
+
'writes':[{'path':p,'before':before,'afterDigest':hashlib.sha256(value).hexdigest()} for p,before,value in writes],
|
|
2274
|
+
'claim':'CONFIGURATION_ONLY_NO_NATIVE_IDENTITY_HEALTH_PERMISSION_OR_ADOPTION_ACK'}
|
|
2275
|
+
digest=hashlib.sha256(canonical(plan)).hexdigest()
|
|
2276
|
+
if operation=='plan':return {**plan,'planDigest':digest}
|
|
2277
|
+
adoption_require(digest==plan_digest,'reviewed plan changed')
|
|
2278
|
+
profile=manifest['profile']
|
|
2279
|
+
adoption_require(adoption_snapshot(manifest_path,profile=profile)[0]==manifest_before,'manifest changed before reservation')
|
|
2280
|
+
from contextlib import ExitStack
|
|
2281
|
+
with ExitStack() as custody:
|
|
2282
|
+
parent_fd=os.open(journal.parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW);custody.callback(os.close,parent_fd)
|
|
2283
|
+
fd=os.open(journal.name,os.O_RDWR|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600,dir_fd=parent_fd);custody.callback(os.close,fd)
|
|
2284
|
+
def generation(st):
|
|
2285
|
+
return (st.st_dev,st.st_ino,st.st_uid,st.st_mode,st.st_nlink,st.st_size,st.st_mtime_ns,st.st_ctime_ns)
|
|
2286
|
+
def parent_identity(st):
|
|
2287
|
+
return (st.st_dev,st.st_ino,st.st_uid,st.st_mode)
|
|
2288
|
+
parent_generation=parent_identity(os.fstat(parent_fd));journal_generation=generation(os.fstat(fd))
|
|
2289
|
+
journal_parents=adoption_snapshot(journal,read=False)[0]['parents']
|
|
2290
|
+
record={'operationId':manifest['operationId'],'planDigest':digest,'state':'may-have-config-effects','completed':[]}
|
|
2291
|
+
def journal_guard():
|
|
2292
|
+
try:
|
|
2293
|
+
current=adoption_snapshot(journal,read=False)[0];held=generation(os.fstat(fd));named=generation(journal.lstat())
|
|
2294
|
+
parent=parent_identity(os.fstat(parent_fd));named_parent=parent_identity(journal.parent.lstat())
|
|
2295
|
+
except OSError as error:
|
|
2296
|
+
raise PackError('Runtime adoption refused: journal custody changed; may have config effects; retain evidence') from error
|
|
2297
|
+
adoption_require(held==named==journal_generation and held[4]==1 and current['mode']==0o600 and
|
|
2298
|
+
parent==named_parent==parent_generation and current['parents']==journal_parents,
|
|
2299
|
+
'journal custody changed; may have config effects; retain evidence')
|
|
2300
|
+
def checkpoint():
|
|
2301
|
+
nonlocal journal_generation
|
|
2302
|
+
journal_guard();data=canonical(record)+b'\n'
|
|
2303
|
+
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0)
|
|
2304
|
+
adoption_require(os.write(fd,data)==len(data),'journal checkpoint incomplete; retain evidence');os.fsync(fd)
|
|
2305
|
+
os.lseek(fd,0,os.SEEK_SET)
|
|
2306
|
+
adoption_require(os.read(fd,len(data)+1)==data,'journal checkpoint differs; retain evidence')
|
|
2307
|
+
updated=generation(os.fstat(fd))
|
|
2308
|
+
adoption_require(updated[:5]==journal_generation[:5],'journal descriptor generation changed; retain evidence')
|
|
2309
|
+
journal_generation=updated;journal_guard()
|
|
2310
|
+
def revalidate():
|
|
2311
|
+
adoption_read_inputs(manifest)
|
|
2312
|
+
adoption_pin(manifest['ownerGeneration'],profile);adoption_profile(profile);adoption_runtime(manifest['runtime'],profile)
|
|
2313
|
+
for keep in manifest['preserve']:adoption_pin(keep,profile)
|
|
2314
|
+
for completed in record['completed']:adoption_require(adoption_snapshot(completed['path'],profile=profile)[0]==completed['after'],'completed file changed')
|
|
2315
|
+
for target in manifest['targets']:
|
|
2316
|
+
for alias in target['aliases']:adoption_alias(alias,target['path'])
|
|
2317
|
+
adoption_require(adoption_snapshot(manifest_path,profile=profile)[0]==manifest_before,'manifest changed during apply')
|
|
2318
|
+
journal_guard()
|
|
2319
|
+
checkpoint()
|
|
2320
|
+
# Recheck the whole completed set at both sides of each replacement,
|
|
2321
|
+
# including the last launcher; prepared is never inferred from one file.
|
|
2322
|
+
for path,before,value in writes:
|
|
2323
|
+
revalidate()
|
|
2324
|
+
current,_=adoption_snapshot(path,missing=True,profile=profile);adoption_require(current==before,'destination changed before write')
|
|
2325
|
+
temporary=path+'.'+manifest['operationId']+'.tmp'
|
|
2326
|
+
mode=before['mode'] if before else (0o700 if path.endswith('/private-mcp-'+manifest['operationId']) else 0o600)
|
|
2327
|
+
adoption_read_guard(profile,[temporary])
|
|
2328
|
+
handle=os.open(temporary,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,mode)
|
|
2329
|
+
with os.fdopen(handle,'wb') as stream:
|
|
2330
|
+
temporary_identity=os.fstat(stream.fileno());os.fchmod(stream.fileno(),mode)
|
|
2331
|
+
stream.write(value);stream.flush();os.fsync(stream.fileno())
|
|
2332
|
+
revalidate()
|
|
2333
|
+
temp,actual=adoption_snapshot(temporary,profile=profile)
|
|
2334
|
+
adoption_require((temp['device'],temp['inode'])==(temporary_identity.st_dev,temporary_identity.st_ino) and actual==value,'temporary custody changed; retain journal')
|
|
2335
|
+
adoption_require(adoption_snapshot(manifest_path,profile=profile)[0]==manifest_before,'manifest changed during apply')
|
|
2336
|
+
current,_=adoption_snapshot(path,missing=True,profile=profile);adoption_require(current==before,'destination changed during write; retained temp and journal')
|
|
2337
|
+
journal_guard();os.replace(temporary,path)
|
|
2338
|
+
after,actual=adoption_snapshot(path,profile=profile);adoption_require(actual==value,'afterimage differs; retain journal')
|
|
2339
|
+
record['completed'].append({'path':path,'after':after});revalidate();checkpoint()
|
|
2340
|
+
revalidate();record['state']='configuration-prepared-native-adoption-unverified';checkpoint();journal_guard()
|
|
2341
|
+
return {'operationId':manifest['operationId'],'planDigest':digest,'state':record['state'],'journal':str(journal),'nativeACK':False}
|
|
2342
|
+
|
|
2343
|
+
|
|
1993
2344
|
def compile_native_cli_command(value,store,pack,input_path,environment):
|
|
1994
2345
|
"""Compile a fixed native tool command; no shell/model text is accepted."""
|
|
1995
2346
|
import shlex
|
|
@@ -5,6 +5,36 @@ from .storage import PackError,atomic_json,canonical,read_json,safe_id
|
|
|
5
5
|
class ConnectionSourceRefused(PackError):
|
|
6
6
|
pass
|
|
7
7
|
|
|
8
|
+
def managed_runtime_selection(path):
|
|
9
|
+
"""Configuration custody only, never a health or native permission ACK."""
|
|
10
|
+
from . import live
|
|
11
|
+
before,raw=live.adoption_snapshot(path)
|
|
12
|
+
live.adoption_require(before['mode']==0o600 and len(raw)<=2*1024*1024,'private runtime descriptor required')
|
|
13
|
+
value=json.loads(raw)
|
|
14
|
+
live.adoption_keys(value,['schemaVersion','library','ownerGeneration','profile','runtime','preserve'])
|
|
15
|
+
live.adoption_require(value['schemaVersion']==1 and type(value['schemaVersion']) is int and
|
|
16
|
+
pathlib.Path(path).parent==pathlib.Path(value['library'])/'launchers','owned runtime descriptor location required')
|
|
17
|
+
live.adoption_read_inputs(value)
|
|
18
|
+
live.adoption_pin(value['ownerGeneration'],value['profile'])
|
|
19
|
+
env=live.adoption_runtime(value['runtime'],value['profile'])
|
|
20
|
+
for pin in value['preserve']:live.adoption_pin(pin,value['profile'])
|
|
21
|
+
live.adoption_require(live.adoption_snapshot(path,profile=value['profile'])[0]==before,'runtime descriptor changed')
|
|
22
|
+
return value,env
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def managed_mcp_main(path,args=()):
|
|
26
|
+
from .live import adoption_require
|
|
27
|
+
adoption_require(len(args)<=1 and all(isinstance(v,str) and re.fullmatch(r'[A-Za-z0-9_-]+',v) for v in args),'closed MCP agent name required')
|
|
28
|
+
value,env=managed_runtime_selection(path)
|
|
29
|
+
if args:env['TOPOLO_AGENT_NAME']=args[0]
|
|
30
|
+
runtime=value['runtime']
|
|
31
|
+
# Only a later owner-admitted ordinary native startup invokes this wrapper.
|
|
32
|
+
# Credential resolution/refresh/identity are real private effects, not preflight.
|
|
33
|
+
# Replace this wrapper, retaining the native host's PID/stdin/cancellation
|
|
34
|
+
# custody instead of leaving an intermediate parent that can orphan MCP.
|
|
35
|
+
return os.execve(runtime['node']['path'],[runtime['node']['path'],runtime['mcp']['path']],env)
|
|
36
|
+
|
|
37
|
+
|
|
8
38
|
class Client:
|
|
9
39
|
def __init__(self,environment,context,binary=None,env=None,node=None):
|
|
10
40
|
if environment not in {'development','staging','production'}:
|
|
@@ -12,6 +42,12 @@ class Client:
|
|
|
12
42
|
if not context or any(c in context for c in '\n\r'):
|
|
13
43
|
raise PackError('Explicit Topolo context required')
|
|
14
44
|
selected=os.environ if env is None else env
|
|
45
|
+
descriptor=selected.get('TOPOLO_AGENT_PACK_RUNTIME')
|
|
46
|
+
if descriptor:
|
|
47
|
+
value,selected=managed_runtime_selection(descriptor)
|
|
48
|
+
if (environment!=value['profile']['environment'] or context!=value['profile']['contextKey'] or binary is not None or node is not None):
|
|
49
|
+
raise ConnectionSourceRefused('Managed private runtime tuple or explicit override differs')
|
|
50
|
+
binary=value['runtime']['cli']['path'];node=value['runtime']['node']['path']
|
|
15
51
|
if any(selected.get(k) for k in ['TOPOLO_API_KEY','TOPOLO_ACCESS_TOKEN','TOPOLO_'+environment.upper()+'_API_KEY','TOPOLO_'+environment.upper()+'_ACCESS_TOKEN']):
|
|
16
52
|
raise PackError('Pinned connections require the CLI protected account context, not injected credentials')
|
|
17
53
|
self.environment=environment;self.context=context
|