opensocrates 1.4.0 → 1.5.1
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 +63 -0
- package/README.ja.md +129 -0
- package/README.ko.md +128 -71
- package/README.md +138 -79
- package/README.zh.md +113 -0
- package/SECURITY.md +49 -6
- package/VERSION +1 -1
- package/installer/managed-hosts.mjs +668 -0
- package/installer/opensocrates.mjs +105 -5
- package/installer/windows.ps1 +124 -5
- package/package.json +5 -4
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
chmod,
|
|
16
16
|
chown,
|
|
17
17
|
cp,
|
|
18
|
+
copyFile,
|
|
18
19
|
lstat,
|
|
19
20
|
mkdir,
|
|
20
21
|
mkdtemp,
|
|
@@ -34,15 +35,16 @@ import { Readable } from "node:stream";
|
|
|
34
35
|
import { pipeline } from "node:stream/promises";
|
|
35
36
|
import { fileURLToPath } from "node:url";
|
|
36
37
|
|
|
37
|
-
export const PRODUCT_VERSION = "1.
|
|
38
|
+
export const PRODUCT_VERSION = "1.5.1";
|
|
38
39
|
export const REPOSITORY = "ParkerHwang/OpenSocrates";
|
|
39
40
|
export const MARKETPLACE_NAME = "opensocrates";
|
|
40
41
|
export const PLUGIN_NAME = "opensocrates";
|
|
41
42
|
export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
42
43
|
export const DEFAULT_HOST = "codex";
|
|
43
44
|
export const SUPPORTED_HOSTS = Object.freeze(["codex"]);
|
|
45
|
+
export const MACOS_PROFILE_HOSTS = Object.freeze(["claude", "antigravity", "claude-chat"]);
|
|
44
46
|
export const ALL_HOST = "all";
|
|
45
|
-
export const HOST_CHOICES = Object.freeze([ALL_HOST, ...SUPPORTED_HOSTS]);
|
|
47
|
+
export const HOST_CHOICES = Object.freeze([ALL_HOST, ...SUPPORTED_HOSTS, ...MACOS_PROFILE_HOSTS]);
|
|
46
48
|
export function assetNameFor(host = DEFAULT_HOST) {
|
|
47
49
|
if (!SUPPORTED_HOSTS.includes(host)) {
|
|
48
50
|
fail(`unsupported host ${JSON.stringify(host)}`);
|
|
@@ -1599,6 +1601,10 @@ export function parseCli(argv) {
|
|
|
1599
1601
|
"verify",
|
|
1600
1602
|
"auto-update",
|
|
1601
1603
|
"help",
|
|
1604
|
+
"diagnose",
|
|
1605
|
+
"disable",
|
|
1606
|
+
"enable",
|
|
1607
|
+
"export",
|
|
1602
1608
|
]);
|
|
1603
1609
|
const args = [...argv];
|
|
1604
1610
|
let action = "install";
|
|
@@ -1635,6 +1641,8 @@ export function parseCli(argv) {
|
|
|
1635
1641
|
force: false,
|
|
1636
1642
|
purge: false,
|
|
1637
1643
|
resetTrust: false,
|
|
1644
|
+
workspace: null,
|
|
1645
|
+
output: null,
|
|
1638
1646
|
};
|
|
1639
1647
|
const seenOptions = new Set();
|
|
1640
1648
|
while (args.length > 0) {
|
|
@@ -1652,6 +1660,12 @@ export function parseCli(argv) {
|
|
|
1652
1660
|
options.host = host;
|
|
1653
1661
|
continue;
|
|
1654
1662
|
}
|
|
1663
|
+
if (flag === "--workspace" || flag === "--output") {
|
|
1664
|
+
const value = args.shift();
|
|
1665
|
+
if (!value || value.startsWith("-")) fail(`${flag} requires a path`);
|
|
1666
|
+
options[flag === "--workspace" ? "workspace" : "output"] = resolve(value);
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1655
1669
|
if (flag === "--allow-major") {
|
|
1656
1670
|
seenOptions.add("allow-major");
|
|
1657
1671
|
options.allowMajor = true;
|
|
@@ -1738,7 +1752,7 @@ export function parseCli(argv) {
|
|
|
1738
1752
|
) {
|
|
1739
1753
|
fail("host-qualified asset options are only valid with --host all");
|
|
1740
1754
|
}
|
|
1741
|
-
if (["status", "remove", "help"].includes(options.action) && options.asset !== null) {
|
|
1755
|
+
if (["status", "remove", "help", "diagnose", "disable", "enable"].includes(options.action) && options.asset !== null) {
|
|
1742
1756
|
fail(`${options.action} does not accept --asset or --checksum`);
|
|
1743
1757
|
}
|
|
1744
1758
|
if (
|
|
@@ -1785,9 +1799,67 @@ export function parseCli(argv) {
|
|
|
1785
1799
|
if (autoUpdateRun && seenOptions.has("host") && options.host !== ALL_HOST) {
|
|
1786
1800
|
fail("auto-update run always reconciles desired state and only accepts --host all");
|
|
1787
1801
|
}
|
|
1802
|
+
if (options.workspace !== null && options.host !== "antigravity") {
|
|
1803
|
+
fail("--workspace is only valid for Antigravity");
|
|
1804
|
+
}
|
|
1805
|
+
if (options.output !== null && !(options.host === "claude-chat" && options.action === "export")) {
|
|
1806
|
+
fail("--output is only valid for the Claude account export");
|
|
1807
|
+
}
|
|
1808
|
+
if (options.host === "claude-chat" && !["export", "verify", "help"].includes(options.action)) {
|
|
1809
|
+
fail("Claude account delivery uses export/verify; account activation is a separate UI action");
|
|
1810
|
+
}
|
|
1811
|
+
if (MACOS_PROFILE_HOSTS.includes(options.host) && (options.purge || options.resetTrust || options.action === "auto-update")) {
|
|
1812
|
+
fail("additional profiles do not broaden purge, trust reset or automatic updates");
|
|
1813
|
+
}
|
|
1814
|
+
if (!MACOS_PROFILE_HOSTS.includes(options.host) && ["diagnose", "disable", "enable", "export"].includes(options.action)) {
|
|
1815
|
+
fail(`${options.action} requires an additional profile`);
|
|
1816
|
+
}
|
|
1788
1817
|
return options;
|
|
1789
1818
|
}
|
|
1790
1819
|
|
|
1820
|
+
function profileAssetName(host) {
|
|
1821
|
+
if (!MACOS_PROFILE_HOSTS.includes(host)) fail("unknown content/native profile");
|
|
1822
|
+
const suffix = host === "claude-chat" ? "skills" : "plugin";
|
|
1823
|
+
return `opensocrates-${PRODUCT_VERSION}-${host}-${suffix}.zip`;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
async function prepareProfilePackage(host, options) {
|
|
1827
|
+
const scratch = await mkdtemp(join(tmpdir(), "opensocrates-profile-"));
|
|
1828
|
+
try {
|
|
1829
|
+
if (process.platform === "win32") windowsAction("seal-new", scratch);
|
|
1830
|
+
const name = profileAssetName(host);
|
|
1831
|
+
let asset = options.asset;
|
|
1832
|
+
let checksum = options.checksum;
|
|
1833
|
+
if (asset === null) {
|
|
1834
|
+
asset = join(scratch, name);
|
|
1835
|
+
checksum = join(scratch, name + ".sha256");
|
|
1836
|
+
const base = `https://github.com/${REPOSITORY}/releases/download/v${PRODUCT_VERSION}`;
|
|
1837
|
+
await downloadFile(base + "/" + name, asset);
|
|
1838
|
+
await downloadFile(base + "/" + name + ".sha256", checksum);
|
|
1839
|
+
}
|
|
1840
|
+
const expected = parseChecksumText(await readFile(checksum, "utf8"), basename(asset));
|
|
1841
|
+
if (await sha256File(asset) !== expected) fail("profile archive checksum mismatch");
|
|
1842
|
+
await requireRegularFileEntry(asset, "profile archive");
|
|
1843
|
+
const captured = join(scratch, "verified.zip");
|
|
1844
|
+
await copyFile(asset, captured, fsConstants.COPYFILE_EXCL);
|
|
1845
|
+
if (await sha256File(captured) !== expected) fail("profile archive changed during preparation");
|
|
1846
|
+
asset = captured;
|
|
1847
|
+
archiveEntries(asset);
|
|
1848
|
+
if (process.platform !== "win32") {
|
|
1849
|
+
const zipinfo = run("/usr/bin/zipinfo", ["-l", asset]);
|
|
1850
|
+
if (/^[lbcps][-rwx]/mu.test(zipinfo.stdout)) fail("profile archive contains non-regular entries");
|
|
1851
|
+
}
|
|
1852
|
+
const root = join(scratch, "package");
|
|
1853
|
+
await extractArchive(asset, root);
|
|
1854
|
+
await walkFiles(root);
|
|
1855
|
+
if (process.platform === "win32") windowsAction("seal-new", root);
|
|
1856
|
+
return { root, asset, cleanup: async () => rm(scratch, { recursive: true, force: true }) };
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
await rm(scratch, { recursive: true, force: true });
|
|
1859
|
+
throw error;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1791
1863
|
function showHelp() {
|
|
1792
1864
|
console.log(`OpenSocrates ${PRODUCT_VERSION}
|
|
1793
1865
|
|
|
@@ -1797,6 +1869,15 @@ Usage:
|
|
|
1797
1869
|
opensocrates update [--host codex|all] [--asset ZIP --checksum FILE]
|
|
1798
1870
|
opensocrates remove [--host codex|all] [--purge [--reset-trust]]
|
|
1799
1871
|
opensocrates verify [--host codex|all] [--asset ZIP --checksum FILE]
|
|
1872
|
+
opensocrates install|update --host claude [--asset ZIP --checksum FILE]
|
|
1873
|
+
opensocrates install|update --host antigravity [--workspace FOLDER]
|
|
1874
|
+
[--asset ZIP --checksum FILE]
|
|
1875
|
+
opensocrates status|diagnose|disable|enable|remove --host claude|antigravity
|
|
1876
|
+
[--workspace FOLDER]
|
|
1877
|
+
opensocrates verify --host claude|antigravity|claude-chat
|
|
1878
|
+
[--asset ZIP --checksum FILE]
|
|
1879
|
+
opensocrates export --host claude-chat --output ZIP
|
|
1880
|
+
[--asset ZIP --checksum FILE]
|
|
1800
1881
|
opensocrates auto-update enable [--host codex|all]
|
|
1801
1882
|
[--channel stable|next] [--interval-hours ${AUTO_UPDATE_DEFAULT_INTERVAL_HOURS}]
|
|
1802
1883
|
[--allow-major]
|
|
@@ -1807,7 +1888,13 @@ Without --asset, install, update, and verify download the v${PRODUCT_VERSION}
|
|
|
1807
1888
|
package and checksum from GitHub Releases. The default lifecycle host is codex.
|
|
1808
1889
|
With --host all, supplied host-qualified asset/checksum pairs define the exact
|
|
1809
1890
|
transaction set and are never mixed with downloads for other hosts. Without
|
|
1810
|
-
qualified assets,
|
|
1891
|
+
qualified assets, ready Codex hosts participate. The additional profiles
|
|
1892
|
+
require an explicit --host; all does not activate them. Automatic updates are opt-in
|
|
1893
|
+
for Codex. Claude native lifecycle requires macOS arm64 and the Claude CLI.
|
|
1894
|
+
Antigravity installs owned rules and skills globally or in the explicit workspace
|
|
1895
|
+
on macOS arm64 and Windows x64. Claude Chat exports a verified portable skill ZIP;
|
|
1896
|
+
upload and account activation happen
|
|
1897
|
+
through Claude's interface. These profiles do not accept purge or reset-trust.
|
|
1811
1898
|
|
|
1812
1899
|
Ordinary remove unregisters the selected host and removes installer-managed
|
|
1813
1900
|
files, but may leave host-owned caches, installer state, and Codex hook trust.
|
|
@@ -4344,9 +4431,22 @@ async function verifyPackages(options) {
|
|
|
4344
4431
|
|
|
4345
4432
|
export async function main(argv = process.argv.slice(2), internalDependencies = {}) {
|
|
4346
4433
|
const options = parseCli(argv);
|
|
4434
|
+
if (MACOS_PROFILE_HOSTS.includes(options.host) && options.action !== "help") {
|
|
4435
|
+
const { runManagedHost } = await import("./managed-hosts.mjs");
|
|
4436
|
+
const operation = () => runManagedHost(options, {
|
|
4437
|
+
version: PRODUCT_VERSION,
|
|
4438
|
+
platform: process.platform,
|
|
4439
|
+
claudeBinary: process.env.CLAUDE_BIN || "claude",
|
|
4440
|
+
preparePackage: prepareProfilePackage,
|
|
4441
|
+
});
|
|
4442
|
+
const result = ["status", "diagnose", "verify", "export"].includes(options.action)
|
|
4443
|
+
? await operation() : await withOperationLock(operation);
|
|
4444
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4445
|
+
return 0;
|
|
4446
|
+
}
|
|
4347
4447
|
if (process.platform === "win32" && options.action === "auto-update") {
|
|
4348
4448
|
const message =
|
|
4349
|
-
|
|
4449
|
+
`Automatic updates: unavailable on Windows in ${PRODUCT_VERSION}. Use opensocrates update --host all manually.`;
|
|
4350
4450
|
if (options.autoUpdateAction === "status" || options.autoUpdateAction === "disable") {
|
|
4351
4451
|
console.log(message);
|
|
4352
4452
|
return 0;
|
package/installer/windows.ps1
CHANGED
|
@@ -1,8 +1,113 @@
|
|
|
1
1
|
# Paths are data in environment variables, never interpolated PowerShell code.
|
|
2
|
-
param([ValidateSet('entries','extract','private')][string]$Action)
|
|
2
|
+
param([ValidateSet('entries','extract','private','check','check-tree','mkdir-private','seal-new','lease')][string]$Action)
|
|
3
3
|
$ErrorActionPreference = 'Stop'
|
|
4
4
|
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
5
|
-
$
|
|
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
|
+
}
|
|
6
111
|
if ($Action -eq 'private') {
|
|
7
112
|
$item = Get-Item -LiteralPath $targetPath -Force
|
|
8
113
|
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Reparse point refused' }
|
|
@@ -20,23 +125,37 @@ if ($Action -eq 'private') {
|
|
|
20
125
|
exit 0
|
|
21
126
|
}
|
|
22
127
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
128
|
+
Assert-Parents $targetPath
|
|
23
129
|
$zip = [IO.Compression.ZipFile]::OpenRead($targetPath)
|
|
24
130
|
try {
|
|
25
131
|
$names = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
132
|
+
$directories = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
26
133
|
$total = 0L
|
|
27
134
|
foreach ($entry in $zip.Entries) {
|
|
28
135
|
$name = $entry.FullName
|
|
136
|
+
if ($name.EndsWith('/')) { [void]$directories.Add($name.TrimEnd('/')) }
|
|
29
137
|
$parts = $name.TrimEnd('/').Split('/')
|
|
30
138
|
if (!$name -or $name.Contains('\') -or $name.StartsWith('/') -or $name -match '[\x00-\x1f<>:"|?*]' -or
|
|
31
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' }
|
|
32
|
-
|
|
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' }
|
|
33
143
|
$total += $entry.Length
|
|
34
144
|
if ($total -gt 2147483648 -or $zip.Entries.Count -gt 20000) { throw 'ZIP limit exceeded' }
|
|
35
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
|
+
}
|
|
36
153
|
if ($Action -eq 'entries') { ConvertTo-Json -InputObject @($zip.Entries | ForEach-Object { $_.FullName }) -Compress; exit 0 }
|
|
37
|
-
$destination =
|
|
154
|
+
$destination = (Native-Path $env:OPENSOCRATES_WINDOWS_DESTINATION).TrimEnd('\') + '\'
|
|
155
|
+
Assert-Parents $destination
|
|
38
156
|
foreach ($entry in $zip.Entries) {
|
|
39
|
-
|
|
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('/','\')))
|
|
40
159
|
if (!$output.StartsWith($destination,[StringComparison]::OrdinalIgnoreCase)) { throw 'ZIP path escape' }
|
|
41
160
|
if ($entry.FullName.EndsWith('/')) { [void][IO.Directory]::CreateDirectory($output); continue }
|
|
42
161
|
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($output))
|
package/package.json
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opensocrates",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.5.1",
|
|
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",
|
|
11
12
|
"installer/windows.ps1",
|
|
12
13
|
"CHANGELOG.md",
|
|
13
14
|
"SECURITY.md",
|
|
14
15
|
"VERSION"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
|
-
"test": "node --test installer/opensocrates.test.mjs installer/lifecycle.test.mjs tools/clean_machine_acceptance.test.mjs tools/reinstall_cycle_acceptance.test.mjs tools/release_gate.test.mjs && npm run test:npx",
|
|
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",
|
|
18
19
|
"test:npx": "node installer/package-smoke.mjs",
|
|
19
20
|
"pack:check": "npm pack --dry-run",
|
|
20
21
|
"prepublishOnly": "npm test",
|
|
21
|
-
"test:windows": "node --test installer/windows.test.mjs"
|
|
22
|
+
"test:windows": "node --test installer/windows.test.mjs installer/managed-windows.test.mjs"
|
|
22
23
|
},
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": ">=20"
|