deadbyte-mcp 0.15.66 → 0.15.68
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/MANIFEST.SHA256 +37 -25
- package/PROVENANCE.json +33 -9
- package/README.txt +20 -20
- package/RELEASE.json +40 -8
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +15 -2
- package/bridge/package.json +1 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R169-NPX-ACTIVE-CONTROLLER-DELEGATION-20261026.md +57 -0
- package/node_modules/which/CHANGELOG.md +166 -0
- package/package.json +4 -2
- package/scripts/r172-protected-preflight.mjs +42 -0
- package/scripts/r173-protected-preflight.mjs +42 -0
- package/src/activation-authority-proof-r75.mjs +28 -4
- package/src/activation-continuity-abort-r172.mjs +108 -0
- package/src/activation-lock-r65.mjs +5 -1
- package/src/activation-preflight-r65.mjs +4 -2
- package/src/active-controller-delegation-r169.mjs +57 -0
- package/src/deadbyte-cli.mjs +18 -0
- package/src/remote-one-shot-coherence-r168.mjs +7 -0
- package/src/version.mjs +1 -1
- package/test/r130-release-security-contract.test.mjs +7 -7
- package/test/r132-release-identity.test.mjs +8 -8
- package/test/r153-prepublish-identity.test.mjs +1 -1
- package/test/r163-isolated-slot-rollback-restore.test.mjs +2 -2
- package/test/r165-live-cli-skew.test.mjs +2 -2
- package/test/r167-release-contract.test.mjs +11 -10
- package/test/r168-protected-entrypoint.test.mjs +12 -12
- package/test/r168-release-contract.test.mjs +5 -15
- package/test/r168-remote-one-shot-coherence.test.mjs +11 -10
- package/test/r169-delegate-active.test.mjs +36 -0
- package/test/r169-delegate-cli-integration.test.mjs +93 -0
- package/test/r169-targeted-runner.mjs +16 -0
- package/test/r172-activation-recovery.test.mjs +95 -0
- package/test/r172-release-contract.test.mjs +35 -0
- package/test/r173-release-contract.test.mjs +32 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r75-authority-evidence.test.mjs +32 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Changes
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## 2.0.2
|
|
5
|
+
|
|
6
|
+
* Rename bin to `node-which`
|
|
7
|
+
|
|
8
|
+
## 2.0.1
|
|
9
|
+
|
|
10
|
+
* generate changelog and publish on version bump
|
|
11
|
+
* enforce 100% test coverage
|
|
12
|
+
* Promise interface
|
|
13
|
+
|
|
14
|
+
## 2.0.0
|
|
15
|
+
|
|
16
|
+
* Parallel tests, modern JavaScript, and drop support for node < 8
|
|
17
|
+
|
|
18
|
+
## 1.3.1
|
|
19
|
+
|
|
20
|
+
* update deps
|
|
21
|
+
* update travis
|
|
22
|
+
|
|
23
|
+
## v1.3.0
|
|
24
|
+
|
|
25
|
+
* Add nothrow option to which.sync
|
|
26
|
+
* update tap
|
|
27
|
+
|
|
28
|
+
## v1.2.14
|
|
29
|
+
|
|
30
|
+
* appveyor: drop node 5 and 0.x
|
|
31
|
+
* travis-ci: add node 6, drop 0.x
|
|
32
|
+
|
|
33
|
+
## v1.2.13
|
|
34
|
+
|
|
35
|
+
* test: Pass missing option to pass on windows
|
|
36
|
+
* update tap
|
|
37
|
+
* update isexe to 2.0.0
|
|
38
|
+
* neveragain.tech pledge request
|
|
39
|
+
|
|
40
|
+
## v1.2.12
|
|
41
|
+
|
|
42
|
+
* Removed unused require
|
|
43
|
+
|
|
44
|
+
## v1.2.11
|
|
45
|
+
|
|
46
|
+
* Prevent changelog script from being included in package
|
|
47
|
+
|
|
48
|
+
## v1.2.10
|
|
49
|
+
|
|
50
|
+
* Use env.PATH only, not env.Path
|
|
51
|
+
|
|
52
|
+
## v1.2.9
|
|
53
|
+
|
|
54
|
+
* fix for paths starting with ../
|
|
55
|
+
* Remove unused `is-absolute` module
|
|
56
|
+
|
|
57
|
+
## v1.2.8
|
|
58
|
+
|
|
59
|
+
* bullet items in changelog that contain (but don't start with) #
|
|
60
|
+
|
|
61
|
+
## v1.2.7
|
|
62
|
+
|
|
63
|
+
* strip 'update changelog' changelog entries out of changelog
|
|
64
|
+
|
|
65
|
+
## v1.2.6
|
|
66
|
+
|
|
67
|
+
* make the changelog bulleted
|
|
68
|
+
|
|
69
|
+
## v1.2.5
|
|
70
|
+
|
|
71
|
+
* make a changelog, and keep it up to date
|
|
72
|
+
* don't include tests in package
|
|
73
|
+
* Properly handle relative-path executables
|
|
74
|
+
* appveyor
|
|
75
|
+
* Attach error code to Not Found error
|
|
76
|
+
* Make tests pass on Windows
|
|
77
|
+
|
|
78
|
+
## v1.2.4
|
|
79
|
+
|
|
80
|
+
* Fix typo
|
|
81
|
+
|
|
82
|
+
## v1.2.3
|
|
83
|
+
|
|
84
|
+
* update isexe, fix regression in pathExt handling
|
|
85
|
+
|
|
86
|
+
## v1.2.2
|
|
87
|
+
|
|
88
|
+
* update deps, use isexe module, test windows
|
|
89
|
+
|
|
90
|
+
## v1.2.1
|
|
91
|
+
|
|
92
|
+
* Sometimes windows PATH entries are quoted
|
|
93
|
+
* Fixed a bug in the check for group and user mode bits. This bug was introduced during refactoring for supporting strict mode.
|
|
94
|
+
* doc cli
|
|
95
|
+
|
|
96
|
+
## v1.2.0
|
|
97
|
+
|
|
98
|
+
* Add support for opt.all and -as cli flags
|
|
99
|
+
* test the bin
|
|
100
|
+
* update travis
|
|
101
|
+
* Allow checking for multiple programs in bin/which
|
|
102
|
+
* tap 2
|
|
103
|
+
|
|
104
|
+
## v1.1.2
|
|
105
|
+
|
|
106
|
+
* travis
|
|
107
|
+
* Refactored and fixed undefined error on Windows
|
|
108
|
+
* Support strict mode
|
|
109
|
+
|
|
110
|
+
## v1.1.1
|
|
111
|
+
|
|
112
|
+
* test +g exes against secondary groups, if available
|
|
113
|
+
* Use windows exe semantics on cygwin & msys
|
|
114
|
+
* cwd should be first in path on win32, not last
|
|
115
|
+
* Handle lower-case 'env.Path' on Windows
|
|
116
|
+
* Update docs
|
|
117
|
+
* use single-quotes
|
|
118
|
+
|
|
119
|
+
## v1.1.0
|
|
120
|
+
|
|
121
|
+
* Add tests, depend on is-absolute
|
|
122
|
+
|
|
123
|
+
## v1.0.9
|
|
124
|
+
|
|
125
|
+
* which.js: root is allowed to execute files owned by anyone
|
|
126
|
+
|
|
127
|
+
## v1.0.8
|
|
128
|
+
|
|
129
|
+
* don't use graceful-fs
|
|
130
|
+
|
|
131
|
+
## v1.0.7
|
|
132
|
+
|
|
133
|
+
* add license to package.json
|
|
134
|
+
|
|
135
|
+
## v1.0.6
|
|
136
|
+
|
|
137
|
+
* isc license
|
|
138
|
+
|
|
139
|
+
## 1.0.5
|
|
140
|
+
|
|
141
|
+
* Awful typo
|
|
142
|
+
|
|
143
|
+
## 1.0.4
|
|
144
|
+
|
|
145
|
+
* Test for path absoluteness properly
|
|
146
|
+
* win: Allow '' as a pathext if cmd has a . in it
|
|
147
|
+
|
|
148
|
+
## 1.0.3
|
|
149
|
+
|
|
150
|
+
* Remove references to execPath
|
|
151
|
+
* Make `which.sync()` work on Windows by honoring the PATHEXT variable.
|
|
152
|
+
* Make `isExe()` always return true on Windows.
|
|
153
|
+
* MIT
|
|
154
|
+
|
|
155
|
+
## 1.0.2
|
|
156
|
+
|
|
157
|
+
* Only files can be exes
|
|
158
|
+
|
|
159
|
+
## 1.0.1
|
|
160
|
+
|
|
161
|
+
* Respect the PATHEXT env for win32 support
|
|
162
|
+
* should 0755 the bin
|
|
163
|
+
* binary
|
|
164
|
+
* guts
|
|
165
|
+
* package
|
|
166
|
+
* 1st
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deadbyte-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.68",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Authenticated MCP agentic runtime with signed skills, persistent model backends, identity-bound remote devices, durable job supervision, immutable release slots, and separately revocable machine authority.",
|
|
@@ -97,7 +97,9 @@
|
|
|
97
97
|
"r167:focused:test": "node --test --test-concurrency=1 test/r167-activation-path.test.mjs test/r167-release-contract.test.mjs test/r167-release-readiness.test.mjs test/r160-release-contract.test.mjs test/r158-epoch-bootstrap.test.mjs test/r65-activation-orchestration.test.mjs",
|
|
98
98
|
"gate:windows": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/gate-windows-r168.ps1 -Stage all",
|
|
99
99
|
"r168:focused:test": "node test/r168-combined-targeted-runner.mjs",
|
|
100
|
-
"r168:release:verify": "node scripts/npm-release-r48.mjs --verify-only"
|
|
100
|
+
"r168:release:verify": "node scripts/npm-release-r48.mjs --verify-only",
|
|
101
|
+
"r172:focused:test": "node --test --test-concurrency=1 test/r172-activation-recovery.test.mjs test/r172-release-contract.test.mjs test/r169-delegate-active.test.mjs test/r169-delegate-cli-integration.test.mjs test/r65-activation-lock-recovery.test.mjs test/r65-activation-orchestration.test.mjs",
|
|
102
|
+
"r173:focused:test": "node --test --test-concurrency=1 test/r75-authority-evidence.test.mjs test/r172-activation-recovery.test.mjs test/r173-release-contract.test.mjs test/r65-activation-preflight.test.mjs test/r65-activation-orchestration.test.mjs"
|
|
101
103
|
},
|
|
102
104
|
"bundleDependencies": [
|
|
103
105
|
"@modelcontextprotocol/server",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// R172 privileged preflight; read-only. Never invokes native CAS or changes grants.
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {readFile} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {parseManifest,verifyManifestTreeExact} from '../src/release-slot.mjs';
|
|
7
|
+
import {inspectTrustedInstallation,privilegedEpochInstallFiles} from '../src/privileged-install-trust-r89.mjs';
|
|
8
|
+
|
|
9
|
+
const root=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
const version='0.15.67',expectedTrain='R172';
|
|
11
|
+
const mandatory=['scripts/r172-protected-preflight.mjs','src/activation-continuity-abort-r172.mjs',
|
|
12
|
+
'src/activation-lock-r65.mjs','src/activation-preflight-r65.mjs',
|
|
13
|
+
'src/activation-reconciliation-journal-r168.mjs','src/activation-reconciliation-terminal-r168.mjs',
|
|
14
|
+
'src/activation-reconciliation-fence-r168.mjs','src/activation-reconciliation-audit-r168.mjs'];
|
|
15
|
+
function denied(reason){throw new Error('R172_'+reason);}
|
|
16
|
+
function actualProtectedRoot(){
|
|
17
|
+
const trusted='C:\\Program Files',rel=path.win32.relative(trusted,root);
|
|
18
|
+
if(!rel||rel==='..'||rel.startsWith('..'+path.win32.sep)||path.win32.isAbsolute(rel))denied('ROOT_UNPROTECTED');
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
async function main(){
|
|
22
|
+
if(process.platform!=='win32'||process.arch!=='x64')denied('WINDOWS_X64_REQUIRED');
|
|
23
|
+
if(process.argv.length!==2)denied('UNKNOWN_OPTION');
|
|
24
|
+
const verifiedRoot=actualProtectedRoot();
|
|
25
|
+
const pkg=JSON.parse(await readFile(path.join(verifiedRoot,'package.json'),'utf8'));
|
|
26
|
+
const release=JSON.parse(await readFile(path.join(verifiedRoot,'RELEASE.json'),'utf8'));
|
|
27
|
+
if(pkg.name!=='deadbyte-mcp'||pkg.version!==version||release.name!=='deadbyte-mcp'||
|
|
28
|
+
release.version!==version||release.train!==expectedTrain)denied('VERSION_DRIFT');
|
|
29
|
+
const manifest=await readFile(path.join(verifiedRoot,'MANIFEST.SHA256'));
|
|
30
|
+
const entries=parseManifest(manifest.toString('utf8'));
|
|
31
|
+
for(const name of mandatory)if(!entries.has(name))denied('SOURCE_NOT_IN_MANIFEST');
|
|
32
|
+
await verifyManifestTreeExact(verifiedRoot,entries,{allowedExtraPaths:['.npmignore','package-lock.json']});
|
|
33
|
+
const important=[...privilegedEpochInstallFiles(verifiedRoot,process.execPath),
|
|
34
|
+
...mandatory.map(rel=>path.win32.join(verifiedRoot,...rel.split('/'))),
|
|
35
|
+
path.win32.join(verifiedRoot,'MANIFEST.SHA256'),path.win32.join(verifiedRoot,'RELEASE.json'),
|
|
36
|
+
path.win32.join(verifiedRoot,'package.json')];
|
|
37
|
+
await inspectTrustedInstallation(important);
|
|
38
|
+
const hash=createHash('sha256').update(manifest).digest('hex');
|
|
39
|
+
process.stdout.write(JSON.stringify({status:'R172_PROTECTED_PREFLIGHT_READONLY_PASS',version,train:expectedTrain,
|
|
40
|
+
manifest_sha256:hash,files:entries.size,terminalization_authorized:false,activation_authorized:false})+'\n');
|
|
41
|
+
}
|
|
42
|
+
try{await main();}catch(error){process.stderr.write('R172_PROTECTED_PREFLIGHT=BLOCKED '+String(error?.message??error)+'\n');process.exitCode=1;}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// R173 privileged preflight; read-only. Never invokes native CAS or changes grants.
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {readFile} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {parseManifest,verifyManifestTreeExact} from '../src/release-slot.mjs';
|
|
7
|
+
import {inspectTrustedInstallation,privilegedEpochInstallFiles} from '../src/privileged-install-trust-r89.mjs';
|
|
8
|
+
|
|
9
|
+
const root=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
const version='0.15.68',expectedTrain='R173';
|
|
11
|
+
const mandatory=['scripts/r173-protected-preflight.mjs','src/activation-continuity-abort-r172.mjs',
|
|
12
|
+
'src/activation-lock-r65.mjs','src/activation-preflight-r65.mjs',
|
|
13
|
+
'src/activation-reconciliation-journal-r168.mjs','src/activation-reconciliation-terminal-r168.mjs',
|
|
14
|
+
'src/activation-reconciliation-fence-r168.mjs','src/activation-reconciliation-audit-r168.mjs'];
|
|
15
|
+
function denied(reason){throw new Error('R173_'+reason);}
|
|
16
|
+
function actualProtectedRoot(){
|
|
17
|
+
const trusted='C:\\Program Files',rel=path.win32.relative(trusted,root);
|
|
18
|
+
if(!rel||rel==='..'||rel.startsWith('..'+path.win32.sep)||path.win32.isAbsolute(rel))denied('ROOT_UNPROTECTED');
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
async function main(){
|
|
22
|
+
if(process.platform!=='win32'||process.arch!=='x64')denied('WINDOWS_X64_REQUIRED');
|
|
23
|
+
if(process.argv.length!==2)denied('UNKNOWN_OPTION');
|
|
24
|
+
const verifiedRoot=actualProtectedRoot();
|
|
25
|
+
const pkg=JSON.parse(await readFile(path.join(verifiedRoot,'package.json'),'utf8'));
|
|
26
|
+
const release=JSON.parse(await readFile(path.join(verifiedRoot,'RELEASE.json'),'utf8'));
|
|
27
|
+
if(pkg.name!=='deadbyte-mcp'||pkg.version!==version||release.name!=='deadbyte-mcp'||
|
|
28
|
+
release.version!==version||release.train!==expectedTrain)denied('VERSION_DRIFT');
|
|
29
|
+
const manifest=await readFile(path.join(verifiedRoot,'MANIFEST.SHA256'));
|
|
30
|
+
const entries=parseManifest(manifest.toString('utf8'));
|
|
31
|
+
for(const name of mandatory)if(!entries.has(name))denied('SOURCE_NOT_IN_MANIFEST');
|
|
32
|
+
await verifyManifestTreeExact(verifiedRoot,entries,{allowedExtraPaths:['.npmignore','package-lock.json']});
|
|
33
|
+
const important=[...privilegedEpochInstallFiles(verifiedRoot,process.execPath),
|
|
34
|
+
...mandatory.map(rel=>path.win32.join(verifiedRoot,...rel.split('/'))),
|
|
35
|
+
path.win32.join(verifiedRoot,'MANIFEST.SHA256'),path.win32.join(verifiedRoot,'RELEASE.json'),
|
|
36
|
+
path.win32.join(verifiedRoot,'package.json')];
|
|
37
|
+
await inspectTrustedInstallation(important);
|
|
38
|
+
const hash=createHash('sha256').update(manifest).digest('hex');
|
|
39
|
+
process.stdout.write(JSON.stringify({status:'R173_PROTECTED_PREFLIGHT_READONLY_PASS',version,train:expectedTrain,
|
|
40
|
+
manifest_sha256:hash,files:entries.size,terminalization_authorized:false,activation_authorized:false})+'\n');
|
|
41
|
+
}
|
|
42
|
+
try{await main();}catch(error){process.stderr.write('R173_PROTECTED_PREFLIGHT=BLOCKED '+String(error?.message??error)+'\n');process.exitCode=1;}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {createHash} from 'node:crypto';
|
|
2
2
|
import {lstat,open,realpath} from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import {createAuthorityEpochStore} from './authority-continuity-r65.mjs';
|
|
4
5
|
|
|
5
6
|
const FIELDS=Object.freeze({process:'grant_file',coding:'lease_file',autonomous:'lease_file',desktop:'grant_file',host:'arm_file'});
|
|
6
7
|
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
@@ -22,7 +23,9 @@ async function readRegularPinned(file,label){
|
|
|
22
23
|
}finally{await handle.close();}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export async function observeAuthorityEvidence({stateRoot,policyPaths,policySha256,authority
|
|
26
|
+
export async function observeAuthorityEvidence({stateRoot,policyPaths,policySha256,authority,
|
|
27
|
+
epochStoreFactory=createAuthorityEpochStore}){
|
|
28
|
+
if(typeof epochStoreFactory!=='function')throw new Error('authority epoch store factory invalid');
|
|
26
29
|
const runtimeRoot=await realpath(path.join(stateRoot,'runtime'));
|
|
27
30
|
const result={};
|
|
28
31
|
for(const [plane,field] of Object.entries(FIELDS)){
|
|
@@ -49,11 +52,31 @@ export async function observeAuthorityEvidence({stateRoot,policyPaths,policySha2
|
|
|
49
52
|
const grantAbsent=grantInfo===undefined;
|
|
50
53
|
if(grantInfo&&(!grantInfo.isFile()||grantInfo.isSymbolicLink()))throw new Error(`${plane} grant file type invalid`);
|
|
51
54
|
const validEntry=entry&&entry.configured===true&&typeof entry.armed==='boolean';
|
|
52
|
-
const
|
|
53
|
-
|
|
55
|
+
const grantBytes=grantInfo?await readRegularPinned(checkedPath,`${plane} grant`):null;
|
|
56
|
+
const grantSha=grantBytes?sha(grantBytes):null;
|
|
57
|
+
let transactionRevoked=false;
|
|
58
|
+
if(validEntry&&!entry.armed&&entry.reason==='epoch-revoked'&&grantBytes){
|
|
59
|
+
let grant;
|
|
60
|
+
try{grant=JSON.parse(grantBytes.toString('utf8').replace(/^\uFEFF/,''));}catch{}
|
|
61
|
+
const nonce=grant?.instance_nonce;
|
|
62
|
+
if(typeof nonce==='string'&&/^[0-9a-f]{64}$/.test(nonce)){
|
|
63
|
+
const store=epochStoreFactory({runtimeDir:parent,policyBytes});
|
|
64
|
+
const [audit,head,verification]=await Promise.all([
|
|
65
|
+
store.auditHistory({plane}),store.readHead({plane}),
|
|
66
|
+
store.verifyGrant({plane,grantBytes,grantInstanceNonce:nonce,policyBytes})
|
|
67
|
+
]);
|
|
68
|
+
transactionRevoked=audit?.status==='PASS'&&Number.isSafeInteger(audit.verified_events)&&audit.verified_events>0&&
|
|
69
|
+
head?.action==='transaction_revoke'&&Number.isSafeInteger(head.seq)&&head.seq>0&&
|
|
70
|
+
/^[0-9a-f]{64}$/.test(String(head.event_sha256??''))&&
|
|
71
|
+
head.policy_sha256===expectedHash&&head.grant_sha256===grantSha&&head.grant_instance_nonce===nonce&&
|
|
72
|
+
/^r65-[0-9a-f]{32}$/.test(String(head.update_id??''))&&verification?.trusted===false&&
|
|
73
|
+
verification?.reason==='epoch-revoked'&&verification?.event_sha256===head.event_sha256;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const validDisarmed=validEntry&&!entry.armed&&(
|
|
77
|
+
(grantAbsent&&(entry.reason==='disarmed'||(plane==='autonomous'&&entry.reason==='coding-disarmed')))||transactionRevoked);
|
|
54
78
|
const validArmed=validEntry&&entry.armed&&!grantAbsent&&entry.reason==='standing-grant'&&
|
|
55
79
|
Number.isSafeInteger(entry.epoch_seq)&&entry.epoch_seq>0&&/^[0-9a-f]{64}$/.test(entry.epoch_sha256??'');
|
|
56
|
-
const grantSha=grantInfo?sha(await readRegularPinned(checkedPath,`${plane} grant`)):null;
|
|
57
80
|
let grantFinal;
|
|
58
81
|
try{grantFinal=await lstat(checkedPath);}
|
|
59
82
|
catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
@@ -61,6 +84,7 @@ export async function observeAuthorityEvidence({stateRoot,policyPaths,policySha2
|
|
|
61
84
|
throw new Error(`${plane} grant identity changed during observation`);
|
|
62
85
|
result[plane]=Object.freeze({state:validDisarmed?'VERIFIED_DISARMED':validArmed?'VERIFIED_ARMED':'UNVERIFIED',
|
|
63
86
|
policy_sha256:expectedHash,grant_absent:grantAbsent,
|
|
87
|
+
...(transactionRevoked?{transaction_revoked:true}:{}),
|
|
64
88
|
...(grantSha?{grant_sha256:grantSha}:{})});
|
|
65
89
|
}
|
|
66
90
|
return Object.freeze(result);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {lstat, readFile, readdir} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {createAuthorityEpochStore} from './authority-continuity-r65.mjs';
|
|
5
|
+
|
|
6
|
+
const PLANES = Object.freeze(['process', 'coding', 'autonomous', 'desktop', 'host']);
|
|
7
|
+
const GRANT_FIELD = Object.freeze({
|
|
8
|
+
process: 'grant_file', coding: 'lease_file', autonomous: 'lease_file',
|
|
9
|
+
desktop: 'grant_file', host: 'arm_file'
|
|
10
|
+
});
|
|
11
|
+
const HEX64 = /^[0-9a-f]{64}$/u;
|
|
12
|
+
const CONTINUITY = /^r65-[0-9a-f]{32}$/u;
|
|
13
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
|
|
15
|
+
function parse(bytes, label) {
|
|
16
|
+
let text = bytes.toString('utf8');
|
|
17
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
18
|
+
try { return JSON.parse(text); }
|
|
19
|
+
catch { throw new Error(`R172 ${label} JSON invalid`); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inside(root, target) {
|
|
23
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
24
|
+
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function optionalBytes(file) {
|
|
28
|
+
try { return await readFile(file); }
|
|
29
|
+
catch (error) { if (error?.code === 'ENOENT') return null; throw error; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function verifyR172PredispatchContinuityAbort({outcome, lock, stateRoot} = {}) {
|
|
33
|
+
if (!outcome || !lock || typeof stateRoot !== 'string' || !path.isAbsolute(stateRoot)) return false;
|
|
34
|
+
if (outcome.schema !== 'deadbyte.r65-activation-outcome.v1' ||
|
|
35
|
+
outcome.claim_operation_id !== lock.operation_id ||
|
|
36
|
+
outcome.preflight_subject_sha256 !== lock.subject_sha256 ||
|
|
37
|
+
outcome.outcome?.state !== 'ACTIVATION_OUTCOME_UNKNOWN' ||
|
|
38
|
+
outcome.outcome?.preflight !== 'AUTHORITY_UNVERIFIED' ||
|
|
39
|
+
outcome.update_id !== null || outcome.state_path !== null ||
|
|
40
|
+
!CONTINUITY.test(String(outcome.continuity_update_id ?? '')) ||
|
|
41
|
+
!HEX64.test(String(outcome.candidate_manifest_sha256 ?? '')) ||
|
|
42
|
+
typeof outcome.candidate_version !== 'string' || typeof outcome.predecessor_release_id !== 'string') return false;
|
|
43
|
+
|
|
44
|
+
const created = Date.parse(lock.created_at_utc), observed = Date.parse(outcome.observed_at_utc);
|
|
45
|
+
if (!Number.isFinite(created) || !Number.isFinite(observed) || observed < created || observed - created > 120_000) return false;
|
|
46
|
+
const state = path.resolve(stateRoot), pointerBytes = await readFile(path.join(state, 'active-release.json'));
|
|
47
|
+
const pointer = parse(pointerBytes, 'active pointer');
|
|
48
|
+
if (pointer.release_id !== outcome.predecessor_release_id ||
|
|
49
|
+
!HEX64.test(String(pointer.manifest_sha256 ?? '')) ||
|
|
50
|
+
!HEX64.test(String(pointer.generation_manifest_sha256 ?? ''))) return false;
|
|
51
|
+
const generation = path.resolve(String(pointer.generation_root ?? ''));
|
|
52
|
+
if (!inside(path.join(state, 'generations'), generation)) return false;
|
|
53
|
+
|
|
54
|
+
const updates = path.join(state, 'evidence', 'updates');
|
|
55
|
+
const candidatePrefix = `r27-${outcome.candidate_manifest_sha256.slice(0, 16)}-`;
|
|
56
|
+
const updateEntries = await readdir(updates, {withFileTypes: true});
|
|
57
|
+
if (updateEntries.some(entry => entry.name.startsWith(candidatePrefix))) return false;
|
|
58
|
+
|
|
59
|
+
const runtime = path.join(state, 'runtime');
|
|
60
|
+
const store = createAuthorityEpochStore({runtimeDir: runtime});
|
|
61
|
+
const revoked = new Map();
|
|
62
|
+
for (const plane of PLANES) {
|
|
63
|
+
const audit = await store.auditHistory({plane});
|
|
64
|
+
if (!['PASS', 'EMPTY'].includes(audit.status)) return false;
|
|
65
|
+
const eventsDir = path.join(runtime, 'authority-epochs', plane, 'events');
|
|
66
|
+
let entries;
|
|
67
|
+
try { entries = await readdir(eventsDir, {withFileTypes: true}); }
|
|
68
|
+
catch (error) { if (error?.code === 'ENOENT' && audit.status === 'EMPTY') entries = []; else throw error; }
|
|
69
|
+
if (entries.length !== audit.verified_events || entries.length > 10_000) return false;
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (!entry.isFile() || entry.isSymbolicLink()) return false;
|
|
72
|
+
const match = /^(\d{16})-([0-9a-f]{64})\.json$/u.exec(entry.name);
|
|
73
|
+
if (!match) return false;
|
|
74
|
+
const bytes = await readFile(path.join(eventsDir, entry.name));
|
|
75
|
+
if (sha(bytes) !== match[2]) return false;
|
|
76
|
+
const event = parse(bytes, `${plane} epoch event`);
|
|
77
|
+
if (event.update_id !== outcome.continuity_update_id) continue;
|
|
78
|
+
const when = Date.parse(event.recorded_at_utc);
|
|
79
|
+
if (event.plane !== plane || event.action !== 'transaction_revoke' ||
|
|
80
|
+
!Number.isFinite(when) || when < created || when > observed || revoked.has(plane)) return false;
|
|
81
|
+
revoked.set(plane, event);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (revoked.size < 1) return false;
|
|
85
|
+
|
|
86
|
+
for (const [plane, event] of revoked) {
|
|
87
|
+
const policyFile = path.join(generation, `${plane}-policy.json`);
|
|
88
|
+
const policyBytes = await readFile(policyFile), policy = parse(policyBytes, `${plane} policy`);
|
|
89
|
+
const grantFile = path.resolve(String(policy?.[GRANT_FIELD[plane]] ?? ''));
|
|
90
|
+
if (!inside(runtime, grantFile)) return false;
|
|
91
|
+
const planeStore = createAuthorityEpochStore({runtimeDir: runtime, policyBytes});
|
|
92
|
+
const head = await planeStore.readHead({plane});
|
|
93
|
+
if (!head || head.seq <= event.seq || head.previous_event_sha256 === null) return false;
|
|
94
|
+
if (head.action === 'user_revoke') {
|
|
95
|
+
if (await optionalBytes(grantFile) !== null) return false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!['grant', 'transaction_restore'].includes(head.action)) return false;
|
|
99
|
+
const grantBytes = await optionalBytes(grantFile);
|
|
100
|
+
if (!grantBytes) return false;
|
|
101
|
+
const grant = parse(grantBytes, `${plane} grant`);
|
|
102
|
+
const verified = await planeStore.verifyGrant({
|
|
103
|
+
plane, grantBytes, grantInstanceNonce: grant.instance_nonce, policyBytes
|
|
104
|
+
});
|
|
105
|
+
if (verified.trusted !== true || verified.event_sha256 !== head.event_sha256) return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
@@ -2,6 +2,7 @@ import {createPublicKey,randomBytes} from 'node:crypto';
|
|
|
2
2
|
import {mkdir,open,readFile,readdir,unlink} from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {verifySlotUpdateEnvelope} from './runtime-update.mjs';
|
|
5
|
+
import {verifyR172PredispatchContinuityAbort} from './activation-continuity-abort-r172.mjs';
|
|
5
6
|
|
|
6
7
|
const HEX64=/^[0-9a-f]{64}$/;
|
|
7
8
|
const OP=/^[0-9a-f]{32}$/;
|
|
@@ -130,8 +131,11 @@ async function findTerminalOutcome(updatesDir,lock,stateRoot){
|
|
|
130
131
|
catch{continue;}
|
|
131
132
|
if(value?.claim_operation_id!==lock.operation_id)continue;
|
|
132
133
|
if(value?.preflight_subject_sha256!==lock.subject_sha256){foreign=true;continue;}
|
|
134
|
+
let continuityAbort=false;
|
|
135
|
+
try{continuityAbort=await verifyR172PredispatchContinuityAbort({outcome:value,lock,stateRoot});}
|
|
136
|
+
catch{continuityAbort=false;}
|
|
133
137
|
if(isSafePreMutationReject(value,lock)||await isSafeTerminalActivation(value,lock,stateRoot)||
|
|
134
|
-
await isSafeSignedPreconditionReject(value,lock,stateRoot))matched=true;
|
|
138
|
+
await isSafeSignedPreconditionReject(value,lock,stateRoot)||continuityAbort)matched=true;
|
|
135
139
|
else unknown=true;
|
|
136
140
|
}
|
|
137
141
|
return {matched,foreign,unknown};
|
|
@@ -55,14 +55,16 @@ export function classifyActivationPreflight({pointer,expectedParent,candidate,ex
|
|
|
55
55
|
for(const plane of PLANES){
|
|
56
56
|
const entry=observation?.authority?.[plane],proof=observation?.authority_proof?.[plane];
|
|
57
57
|
if(entry&&typeof entry==='object'){
|
|
58
|
-
const knownReason=entry.armed?entry.reason==='standing-grant':
|
|
58
|
+
const knownReason=entry.armed?entry.reason==='standing-grant':
|
|
59
|
+
entry.reason==='disarmed'||entry.reason==='coding-disarmed'||entry.reason==='epoch-revoked';
|
|
59
60
|
if(!knownReason)return Object.freeze({state:'AUTHORITY_UNVERIFIED',safe_to_activate:false,reason:`${plane} authority reason unverified`,...bound});
|
|
60
61
|
}
|
|
61
62
|
if(!proof)
|
|
62
63
|
return Object.freeze({state:'AUTHORITY_UNVERIFIED',safe_to_activate:false,reason:`${plane} authority proof missing`,...bound});
|
|
63
64
|
if(proof&&(proof.policy_sha256!==observation?.policy_sha256?.[plane]||
|
|
64
65
|
proof.state!==(authority[plane]?'VERIFIED_ARMED':'VERIFIED_DISARMED')||
|
|
65
|
-
(!authority[plane]&&proof.grant_absent!==true
|
|
66
|
+
(!authority[plane]&&proof.grant_absent!==true&&
|
|
67
|
+
!(entry?.reason==='epoch-revoked'&&proof.transaction_revoked===true))))
|
|
66
68
|
return Object.freeze({state:'AUTHORITY_UNVERIFIED',safe_to_activate:false,reason:`${plane} authority proof unverified`,...bound});
|
|
67
69
|
}
|
|
68
70
|
if(PLANES.some(plane=>!HEX64.test(String(observation?.policy_sha256?.[plane]))))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// R169 pre-dispatch identity fence. Only routes an explicit user-issued
|
|
2
|
+
// authority command to the already installed controller, never upgrades or
|
|
3
|
+
// fabricates an authority grant.
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {lstat,readFile,realpath} from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
function deny(condition,reason){if(!condition)throw new Error('R169 installed controller '+reason);}
|
|
11
|
+
const canonical=pathName=>path.win32.normalize(path.win32.resolve(pathName)).toLowerCase();
|
|
12
|
+
function descendant(parent,child){
|
|
13
|
+
const r=path.win32.relative(parent,child);
|
|
14
|
+
return r!==''&&r!=='..'&&!r.startsWith('..'+path.win32.sep)&&!path.win32.isAbsolute(r);
|
|
15
|
+
}
|
|
16
|
+
async function fileBytes(file){
|
|
17
|
+
const before=await lstat(file);
|
|
18
|
+
deny(before.isFile()&&!before.isSymbolicLink()&&before.size>0&&before.size<=8*1024*1024,
|
|
19
|
+
'file not regular or bounded');
|
|
20
|
+
deny(canonical(await realpath(file))===canonical(file),'file reparse not allowed');
|
|
21
|
+
const bytes=await readFile(file);
|
|
22
|
+
deny(bytes.length===before.size,'file length changed');
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
export async function assertR169PinnedActiveDelegation({stateRoot,launcherPath,active}={}){
|
|
26
|
+
deny(process.platform==='win32','Windows only');
|
|
27
|
+
deny(typeof stateRoot==='string'&&path.win32.isAbsolute(stateRoot),'state root invalid');
|
|
28
|
+
deny(active&&typeof active==='object'&&
|
|
29
|
+
SHA.test(String(active.manifest_sha256))&&
|
|
30
|
+
SHA.test(String(active.generation_manifest_sha256))&&
|
|
31
|
+
SHA.test(String(active.launcher_sha256))&&
|
|
32
|
+
typeof active.release_id==='string'&&
|
|
33
|
+
typeof active.release_root==='string'&&
|
|
34
|
+
typeof active.generation_root==='string'&&
|
|
35
|
+
typeof active.config_path==='string','active release binding incomplete');
|
|
36
|
+
const root=path.win32.resolve(stateRoot);
|
|
37
|
+
const expectedLauncher=path.win32.join(root,'bin','DEADBYTE-MCP.ps1');
|
|
38
|
+
deny(canonical(launcherPath)===canonical(expectedLauncher),'launcher path not active stable launcher');
|
|
39
|
+
deny(descendant(path.win32.join(root,'releases'),active.release_root)&&
|
|
40
|
+
descendant(path.win32.join(root,'generations'),active.generation_root),
|
|
41
|
+
'release/generation escapes state root');
|
|
42
|
+
deny(canonical(active.config_path)===canonical(path.win32.join(active.generation_root,'controller-config.psd1')),
|
|
43
|
+
'generation controller config not bound');
|
|
44
|
+
const launcher=await fileBytes(launcherPath);
|
|
45
|
+
deny(hash(launcher)===active.launcher_sha256,'launcher digest mismatch');
|
|
46
|
+
const releaseManifest=await fileBytes(path.win32.join(active.release_root,'MANIFEST.SHA256'));
|
|
47
|
+
deny(hash(releaseManifest)===active.manifest_sha256,'release manifest digest mismatch');
|
|
48
|
+
const generationManifest=await fileBytes(path.win32.join(active.generation_root,'GENERATION.SHA256'));
|
|
49
|
+
deny(hash(generationManifest)===active.generation_manifest_sha256,
|
|
50
|
+
'generation manifest digest mismatch');
|
|
51
|
+
// Installed PowerShell launcher enforces its own identity/generation/
|
|
52
|
+
// controller provenance again before invoking the actual controller.
|
|
53
|
+
return Object.freeze({release_id:active.release_id,version:active.version,
|
|
54
|
+
launcher_sha256:active.launcher_sha256,manifest_sha256:active.manifest_sha256,
|
|
55
|
+
generation_manifest_sha256:active.generation_manifest_sha256,
|
|
56
|
+
activation_attempted:false});
|
|
57
|
+
}
|
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { classifyRemoteAuditOutcome, createLiveTimelineFilter, createLiveTimelin
|
|
|
20
20
|
import { prepareRemoteConsole, remoteDisplayText } from './remote-console.mjs';
|
|
21
21
|
import { ensureRuntimeCoherence } from './runtime-coherence.mjs';
|
|
22
22
|
import {selectR168OneShotRemotePath} from './remote-one-shot-coherence-r168.mjs';
|
|
23
|
+
import {assertR169PinnedActiveDelegation} from './active-controller-delegation-r169.mjs';
|
|
23
24
|
import { windowsPowerShellEnvironment } from './runtime-update.mjs';
|
|
24
25
|
import { resolveStateRoot } from './state-root.mjs';
|
|
25
26
|
import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentity,proveRestrictedPidReuse} from './foreground-process-identity.mjs';
|
|
@@ -960,6 +961,23 @@ async function runRemote(args){
|
|
|
960
961
|
const active=await readActiveRuntimeBinding(ctx.root);
|
|
961
962
|
const decision=selectR168OneShotRemotePath({command,active,cliVersion:DEADBYTE_VERSION});
|
|
962
963
|
if(decision.mode==='blocked_version_skew') throw new Error(decision.message);
|
|
964
|
+
if(decision.mode==='delegate_active'){
|
|
965
|
+
// R169 explicitly delegates ONLY plane arm/disarm verbs to the installed
|
|
966
|
+
// controller. The launcher verifies its own identity and policy; this path
|
|
967
|
+
// never launches deploy-r48.mjs or attempts runtime coherence activation.
|
|
968
|
+
await assertR169PinnedActiveDelegation({stateRoot:ctx.root,launcherPath:ctx.launcher,active});
|
|
969
|
+
const unchanged=await readActiveRuntimeBinding(ctx.root);
|
|
970
|
+
if(unchanged?.release_id!==active.release_id||
|
|
971
|
+
unchanged?.generation_manifest_sha256!==active.generation_manifest_sha256)
|
|
972
|
+
throw new Error('R169 active runtime changed before delegated command; no dispatch');
|
|
973
|
+
const result=invokeController(ctx,command,{allowFailure:true,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
974
|
+
const final=await readActiveRuntimeBinding(ctx.root);
|
|
975
|
+
if(final?.release_id!==active.release_id||
|
|
976
|
+
final?.generation_manifest_sha256!==active.generation_manifest_sha256)
|
|
977
|
+
throw new Error('R169 active runtime changed during delegated command; outcome unknown');
|
|
978
|
+
process.exitCode=result.status;
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
963
981
|
if(decision.mode==='observe_active'||decision.mode==='observe_unbound'){
|
|
964
982
|
if(decision.mode==='observe_active')await reportVersionCoherence(ctx.root,{displayMode:'ascii'});
|
|
965
983
|
const observed=invokeController(ctx,command,{allowFailure:true,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
@@ -9,6 +9,8 @@ const COMMANDS=new Set([
|
|
|
9
9
|
'armautonomous','disarmautonomous','autonomousstatus','autonomoustest'
|
|
10
10
|
]);
|
|
11
11
|
const OBSERVE=new Set(['status','processstatus','codingstatus','autonomousstatus','desktopstatus','hoststatus']);
|
|
12
|
+
const DELEGATE=new Set(['armprocess','disarmprocess','armcoding','disarmcoding','armautonomous',
|
|
13
|
+
'disarmautonomous','armdesktop','disarmdesktop','armhost','disarmhost']);
|
|
12
14
|
const safe=(mode,active,cli,message=null)=>Object.freeze({
|
|
13
15
|
mode,active_version:active,cli_version:cli,message,
|
|
14
16
|
may_activate:false,may_mutate_authority:false
|
|
@@ -31,6 +33,11 @@ export function selectR168OneShotRemotePath({command,active,cliVersion}={}){
|
|
|
31
33
|
'R168 version skew: installed runtime '+active.version+', npm CLI '+cliVersion+
|
|
32
34
|
'. Automatic activation from npm cache is forbidden. Existing runtime remains unchanged.';
|
|
33
35
|
if(OBSERVE.has(command))return safe('observe_active',active.version,cliVersion,explanation);
|
|
36
|
+
if(relation>0&&DELEGATE.has(command))return Object.freeze({
|
|
37
|
+
...safe('delegate_active',active.version,cliVersion,
|
|
38
|
+
explanation+' Forwarding explicit plane authority request to installed active controller.'),
|
|
39
|
+
may_mutate_authority:true
|
|
40
|
+
});
|
|
34
41
|
return safe('blocked_version_skew',active.version,cliVersion,
|
|
35
42
|
explanation+' Run the corresponding command through the installed active-release launcher only if specifically authorized.');
|
|
36
43
|
}
|
package/src/version.mjs
CHANGED
|
@@ -7,18 +7,18 @@ import {DEADBYTE_VERSION} from '../src/version.mjs';
|
|
|
7
7
|
|
|
8
8
|
const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
|
|
9
9
|
const read=async file=>JSON.parse(await readFile(path.join(root,file),'utf8'));
|
|
10
|
-
test('current successor retains the R130 security baseline with truthful
|
|
10
|
+
test('current successor retains the R130 security baseline with truthful R173 identity',async()=>{
|
|
11
11
|
const [pkg,release,provenance,bom,lock,matrix]=await Promise.all([
|
|
12
12
|
read('package.json'),read('RELEASE.json'),read('PROVENANCE.json'),
|
|
13
13
|
read('SBOM.cdx.json'),read('package-lock.json'),read('TEST-MATRIX.json')]);
|
|
14
14
|
assert.equal(pkg.name,'deadbyte-mcp');
|
|
15
15
|
for(const actual of [pkg.version,release.version,provenance.version,matrix.version,bom.metadata.component.version,lock.version,lock.packages[''].version,DEADBYTE_VERSION])
|
|
16
|
-
assert.equal(actual,'0.15.
|
|
17
|
-
assert.equal(release.train,'
|
|
18
|
-
assert.equal(matrix.train,'
|
|
19
|
-
assert.equal(release.source_predecessor.version,'0.15.
|
|
20
|
-
assert.equal(release.source_predecessor.train,'
|
|
21
|
-
assert.match(release.source_predecessor.source_path,/0\.15\.
|
|
16
|
+
assert.equal(actual,'0.15.68');
|
|
17
|
+
assert.equal(release.train,'R173');
|
|
18
|
+
assert.equal(matrix.train,'R173');
|
|
19
|
+
assert.equal(release.source_predecessor.version,'0.15.67');
|
|
20
|
+
assert.equal(release.source_predecessor.train,'R172');
|
|
21
|
+
assert.match(release.source_predecessor.source_path,/0\.15\.67-r172-final-remote-recovery/i);
|
|
22
22
|
assert.equal(release.source_predecessor.manifest_status,'VERIFIED_FINAL_BYTES');
|
|
23
23
|
assert.equal(release.runtime_predecessor.version,'0.15.61');
|
|
24
24
|
assert.equal(provenance.r160.source_predecessor_manifest_status,'STALE_REJECTED');
|