opensocrates 1.3.1 → 1.5.0
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/CHANGELOG.md +60 -0
- package/README.ko.md +90 -82
- package/README.md +93 -88
- package/SECURITY.md +102 -141
- package/VERSION +1 -1
- package/installer/managed-hosts.mjs +659 -0
- package/installer/opensocrates.mjs +686 -1362
- package/installer/windows.ps1 +164 -0
- package/package.json +7 -12
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Paths are data in environment variables, never interpolated PowerShell code.
|
|
2
|
+
param([ValidateSet('entries','extract','private','check','check-tree','mkdir-private','seal-new','lease')][string]$Action)
|
|
3
|
+
$ErrorActionPreference = 'Stop'
|
|
4
|
+
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
5
|
+
function Native-Path([string]$path) {
|
|
6
|
+
$absolute = [IO.Path]::GetFullPath($path)
|
|
7
|
+
# PowerShell 5.1's FileSystemInfo ACL methods otherwise fail beyond MAX_PATH,
|
|
8
|
+
# including an owned tree temporarily moved below a transaction backup.
|
|
9
|
+
# Convert an already absolute local drive path; caller path/alias policy and
|
|
10
|
+
# every ownership, DACL and reparse check remain in force.
|
|
11
|
+
if ($absolute -match '^[a-zA-Z]:\\') { return '\\?\' + $absolute }
|
|
12
|
+
return $absolute
|
|
13
|
+
}
|
|
14
|
+
$targetPath = Native-Path $env:OPENSOCRATES_WINDOWS_PATH
|
|
15
|
+
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
16
|
+
$currentSid = $currentIdentity.User
|
|
17
|
+
$trustedSids = @($currentSid.Value,'S-1-5-18','S-1-5-32-544')
|
|
18
|
+
function Assert-Parents([string]$path) {
|
|
19
|
+
$cursor = [IO.Path]::GetFullPath($path)
|
|
20
|
+
while ($cursor) {
|
|
21
|
+
$item = Get-Item -LiteralPath $cursor -Force
|
|
22
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Reparse ancestor refused' }
|
|
23
|
+
$cursor = [IO.Path]::GetDirectoryName($cursor)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function Private-Acl([bool]$directory) {
|
|
27
|
+
$acl = if ($directory) { [Security.AccessControl.DirectorySecurity]::new() } else { [Security.AccessControl.FileSecurity]::new() }
|
|
28
|
+
$acl.SetOwner($currentSid)
|
|
29
|
+
$acl.SetAccessRuleProtection($true,$false)
|
|
30
|
+
$inherit = if ($directory) { 'ContainerInherit,ObjectInherit' } else { 'None' }
|
|
31
|
+
foreach ($identity in $trustedSids) {
|
|
32
|
+
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new($identity),'FullControl',$inherit,'None','Allow'))
|
|
33
|
+
}
|
|
34
|
+
return $acl
|
|
35
|
+
}
|
|
36
|
+
function Assert-Owned($item) {
|
|
37
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Reparse point refused' }
|
|
38
|
+
$acl = $item.GetAccessControl()
|
|
39
|
+
if ($acl.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $currentSid.Value) { throw 'Foreign owner refused' }
|
|
40
|
+
$writes = [Security.AccessControl.FileSystemRights]::Write -bor [Security.AccessControl.FileSystemRights]::Delete -bor [Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor [Security.AccessControl.FileSystemRights]::ChangePermissions -bor [Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
41
|
+
foreach ($rule in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])) {
|
|
42
|
+
if ($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) { continue }
|
|
43
|
+
if ($rule.AccessControlType -eq 'Allow' -and ($rule.FileSystemRights -band $writes) -and $rule.IdentityReference.Value -notin $trustedSids) { throw 'Untrusted writable DACL refused' }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function Tree-Items($rootItem) {
|
|
47
|
+
$pending = [Collections.Generic.Queue[IO.FileSystemInfo]]::new()
|
|
48
|
+
$pending.Enqueue($rootItem)
|
|
49
|
+
$count = 0
|
|
50
|
+
while ($pending.Count) {
|
|
51
|
+
$item = $pending.Dequeue()
|
|
52
|
+
if (++$count -gt 20000) { throw 'Filesystem limit exceeded' }
|
|
53
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Reparse point refused' }
|
|
54
|
+
$item
|
|
55
|
+
if ($item -is [IO.DirectoryInfo]) {
|
|
56
|
+
foreach ($child in $item.EnumerateFileSystemInfos()) { $pending.Enqueue($child) }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if ($Action -eq 'mkdir-private') {
|
|
61
|
+
if (Test-Path -LiteralPath $targetPath) { throw 'New private directory collision' }
|
|
62
|
+
Assert-Parents ([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($targetPath)))
|
|
63
|
+
([IO.DirectoryInfo]::new($targetPath)).Create((Private-Acl $true))
|
|
64
|
+
Assert-Owned (Get-Item -LiteralPath $targetPath -Force)
|
|
65
|
+
exit 0
|
|
66
|
+
}
|
|
67
|
+
if ($Action -in @('check','check-tree','seal-new','lease')) {
|
|
68
|
+
Assert-Parents $targetPath
|
|
69
|
+
$rootItem = Get-Item -LiteralPath $targetPath -Force
|
|
70
|
+
if ($Action -eq 'seal-new') {
|
|
71
|
+
# Caller uses this only for an exclusively created staging tree/file.
|
|
72
|
+
$items = @(Tree-Items $rootItem)
|
|
73
|
+
foreach ($item in $items) {
|
|
74
|
+
$owner = $item.GetAccessControl().GetOwner([Security.Principal.SecurityIdentifier]).Value
|
|
75
|
+
if ($owner -notin @($currentSid.Value,$currentIdentity.Owner.Value)) { throw 'Foreign staging owner refused' }
|
|
76
|
+
}
|
|
77
|
+
foreach ($item in $items) { $item.SetAccessControl((Private-Acl ($item -is [IO.DirectoryInfo]))) }
|
|
78
|
+
} elseif ($Action -eq 'check-tree') {
|
|
79
|
+
foreach ($item in (Tree-Items $rootItem)) { Assert-Owned $item }
|
|
80
|
+
} else { Assert-Owned $rootItem }
|
|
81
|
+
if ($Action -ne 'lease') { exit 0 }
|
|
82
|
+
# Pin ancestors without FILE_SHARE_DELETE while Node holds its operation lock.
|
|
83
|
+
Add-Type @'
|
|
84
|
+
using System;
|
|
85
|
+
using System.Runtime.InteropServices;
|
|
86
|
+
using Microsoft.Win32.SafeHandles;
|
|
87
|
+
public static class OpenSocratesDirectoryLease {
|
|
88
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
89
|
+
public static extern SafeFileHandle CreateFile(string name, uint access, uint share, IntPtr security, uint mode, uint flags, IntPtr template);
|
|
90
|
+
}
|
|
91
|
+
'@
|
|
92
|
+
$handles = [Collections.Generic.List[Microsoft.Win32.SafeHandles.SafeFileHandle]]::new()
|
|
93
|
+
try {
|
|
94
|
+
$cursor = [IO.Path]::GetFullPath($targetPath)
|
|
95
|
+
while ($cursor) {
|
|
96
|
+
# FILE_LIST_DIRECTORY establishes read sharing; access=0 does not
|
|
97
|
+
# participate in deletion sharing and cannot protect against rename.
|
|
98
|
+
$handle = [OpenSocratesDirectoryLease]::CreateFile($cursor,1,3,[IntPtr]::Zero,3,0x02200000,[IntPtr]::Zero)
|
|
99
|
+
if ($handle.IsInvalid) { throw 'Cannot pin managed ancestor' }
|
|
100
|
+
$handles.Add($handle)
|
|
101
|
+
$cursor = [IO.Path]::GetDirectoryName($cursor)
|
|
102
|
+
}
|
|
103
|
+
Assert-Parents $targetPath
|
|
104
|
+
Assert-Owned (Get-Item -LiteralPath $targetPath -Force)
|
|
105
|
+
[Console]::WriteLine('ready')
|
|
106
|
+
[Console]::Out.Flush()
|
|
107
|
+
[void][Console]::In.ReadLine()
|
|
108
|
+
} finally { foreach ($handle in $handles) { $handle.Dispose() } }
|
|
109
|
+
exit 0
|
|
110
|
+
}
|
|
111
|
+
if ($Action -eq 'private') {
|
|
112
|
+
$item = Get-Item -LiteralPath $targetPath -Force
|
|
113
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Reparse point refused' }
|
|
114
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
115
|
+
$acl = $item.GetAccessControl()
|
|
116
|
+
if ($acl.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'Foreign owner refused' }
|
|
117
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
118
|
+
foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) }
|
|
119
|
+
$inherit = if ($item.PSIsContainer) { 'ContainerInherit,ObjectInherit' } else { 'None' }
|
|
120
|
+
foreach ($identity in @($sid.Value,'S-1-5-18','S-1-5-32-544')) {
|
|
121
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new($identity), 'FullControl', $inherit, 'None', 'Allow')
|
|
122
|
+
$acl.AddAccessRule($rule)
|
|
123
|
+
}
|
|
124
|
+
$item.SetAccessControl($acl)
|
|
125
|
+
exit 0
|
|
126
|
+
}
|
|
127
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
128
|
+
Assert-Parents $targetPath
|
|
129
|
+
$zip = [IO.Compression.ZipFile]::OpenRead($targetPath)
|
|
130
|
+
try {
|
|
131
|
+
$names = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
132
|
+
$directories = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
133
|
+
$total = 0L
|
|
134
|
+
foreach ($entry in $zip.Entries) {
|
|
135
|
+
$name = $entry.FullName
|
|
136
|
+
if ($name.EndsWith('/')) { [void]$directories.Add($name.TrimEnd('/')) }
|
|
137
|
+
$parts = $name.TrimEnd('/').Split('/')
|
|
138
|
+
if (!$name -or $name.Contains('\') -or $name.StartsWith('/') -or $name -match '[\x00-\x1f<>:"|?*]' -or
|
|
139
|
+
($parts | Where-Object { !$_ -or $_ -eq '.' -or $_ -eq '..' -or $_ -match '[. ]$' -or $_ -match '^(?i:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)' }) -or !$names.Add($name.TrimEnd('/'))) { throw 'Unsafe or duplicate ZIP path' }
|
|
140
|
+
$kind = ($entry.ExternalAttributes -shr 16) -band 0xF000
|
|
141
|
+
if ($kind -notin @(0,0x4000,0x8000) -or ($entry.ExternalAttributes -band 0x400)) { throw 'ZIP non-regular entry refused' }
|
|
142
|
+
if ($kind -eq 0x4000 -and !$name.EndsWith('/')) { throw 'ZIP directory type mismatch' }
|
|
143
|
+
$total += $entry.Length
|
|
144
|
+
if ($total -gt 2147483648 -or $zip.Entries.Count -gt 20000) { throw 'ZIP limit exceeded' }
|
|
145
|
+
}
|
|
146
|
+
foreach ($entry in $zip.Entries) {
|
|
147
|
+
$parentName = $entry.FullName.TrimEnd('/')
|
|
148
|
+
while ($parentName.Contains('/')) {
|
|
149
|
+
$parentName = $parentName.Substring(0,$parentName.LastIndexOf('/'))
|
|
150
|
+
if ($names.Contains($parentName) -and !$directories.Contains($parentName)) { throw 'ZIP file-directory collision' }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ($Action -eq 'entries') { ConvertTo-Json -InputObject @($zip.Entries | ForEach-Object { $_.FullName }) -Compress; exit 0 }
|
|
154
|
+
$destination = (Native-Path $env:OPENSOCRATES_WINDOWS_DESTINATION).TrimEnd('\') + '\'
|
|
155
|
+
Assert-Parents $destination
|
|
156
|
+
foreach ($entry in $zip.Entries) {
|
|
157
|
+
# Extended Win32 paths do not translate the ZIP's portable slash syntax.
|
|
158
|
+
$output = [IO.Path]::GetFullPath([IO.Path]::Combine($destination,$entry.FullName.Replace('/','\')))
|
|
159
|
+
if (!$output.StartsWith($destination,[StringComparison]::OrdinalIgnoreCase)) { throw 'ZIP path escape' }
|
|
160
|
+
if ($entry.FullName.EndsWith('/')) { [void][IO.Directory]::CreateDirectory($output); continue }
|
|
161
|
+
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($output))
|
|
162
|
+
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry,$output,$false)
|
|
163
|
+
}
|
|
164
|
+
} finally { $zip.Dispose() }
|
package/package.json
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opensocrates",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Original reasoning assistance with Codex, Claude account skills and Antigravity delivery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opensocrates": "installer/opensocrates.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"installer/opensocrates.mjs",
|
|
11
|
+
"installer/managed-hosts.mjs",
|
|
12
|
+
"installer/windows.ps1",
|
|
11
13
|
"CHANGELOG.md",
|
|
12
14
|
"SECURITY.md",
|
|
13
15
|
"VERSION"
|
|
14
16
|
],
|
|
15
17
|
"scripts": {
|
|
16
|
-
"test": "node --test installer/opensocrates.test.mjs installer/lifecycle.test.mjs
|
|
18
|
+
"test": "node --test installer/opensocrates.test.mjs installer/lifecycle.test.mjs installer/managed-hosts.test.mjs tools/clean_machine_acceptance.test.mjs tools/reinstall_cycle_acceptance.test.mjs tools/release_gate.test.mjs && npm run test:npx",
|
|
17
19
|
"test:npx": "node installer/package-smoke.mjs",
|
|
18
20
|
"pack:check": "npm pack --dry-run",
|
|
19
|
-
"prepublishOnly": "npm test"
|
|
21
|
+
"prepublishOnly": "npm test",
|
|
22
|
+
"test:windows": "node --test installer/windows.test.mjs installer/managed-windows.test.mjs"
|
|
20
23
|
},
|
|
21
24
|
"engines": {
|
|
22
25
|
"node": ">=20"
|
|
@@ -36,17 +39,9 @@
|
|
|
36
39
|
},
|
|
37
40
|
"keywords": [
|
|
38
41
|
"ai",
|
|
39
|
-
"claude",
|
|
40
|
-
"claude-code",
|
|
41
42
|
"codex",
|
|
42
|
-
"grok",
|
|
43
|
-
"grok-build",
|
|
44
|
-
"cowork",
|
|
45
|
-
"cursor",
|
|
46
|
-
"opencode",
|
|
47
43
|
"reasoning",
|
|
48
44
|
"agent",
|
|
49
|
-
"antigravity",
|
|
50
45
|
"installer",
|
|
51
46
|
"critical-thinking",
|
|
52
47
|
"socratic"
|