@the-open-engine-company/zeroshot 10.8.0 → 10.9.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/lib/install.js CHANGED
@@ -7,7 +7,7 @@ const { URL } = require('url');
7
7
  const { installSkills } = require('./skills');
8
8
  const {
9
9
  archiveName,
10
- extractExecutable,
10
+ extractExecutables,
11
11
  parseChecksumManifest,
12
12
  targetForHost,
13
13
  verifyArchive,
@@ -25,8 +25,40 @@ const HOST_TARGETS = Object.freeze(
25
25
  );
26
26
 
27
27
  function selectTarget(platform = process.platform, arch = process.arch) {
28
- const { target, executable } = targetForHost(TARGETS, platform, arch);
29
- return { target, executable };
28
+ const { target, executable, resticExecutable } = targetForHost(TARGETS, platform, arch);
29
+ return { target, executable, resticExecutable };
30
+ }
31
+
32
+ function installExecutables(nativeDirectory, entries) {
33
+ fs.mkdirSync(nativeDirectory, { recursive: true });
34
+ const stage = fs.mkdtempSync(path.join(nativeDirectory, '.install-'));
35
+ const replaced = [];
36
+ const backups = [];
37
+ try {
38
+ for (const [name, contents] of entries) {
39
+ fs.writeFileSync(path.join(stage, name), contents, { mode: 0o755, flag: 'wx' });
40
+ }
41
+ for (const [name] of entries) {
42
+ const destination = path.join(nativeDirectory, name);
43
+ if (fs.existsSync(destination)) {
44
+ const backup = path.join(stage, `${name}.previous`);
45
+ fs.renameSync(destination, backup);
46
+ backups.push([destination, backup]);
47
+ }
48
+ }
49
+ for (const [name] of entries) {
50
+ const destination = path.join(nativeDirectory, name);
51
+ fs.renameSync(path.join(stage, name), destination);
52
+ replaced.push(destination);
53
+ if (process.platform !== 'win32') fs.chmodSync(destination, 0o755);
54
+ }
55
+ } catch (error) {
56
+ for (const destination of replaced.reverse()) fs.rmSync(destination, { force: true });
57
+ for (const [destination, backup] of backups.reverse()) fs.renameSync(backup, destination);
58
+ throw error;
59
+ } finally {
60
+ fs.rmSync(stage, { recursive: true, force: true });
61
+ }
30
62
  }
31
63
 
32
64
  function download(url, maximumBytes, redirects = 0) {
@@ -108,19 +140,11 @@ async function install(options = {}) {
108
140
  const manifest = await fetchBuffer(`${baseUrl}/SHA256SUMS`, MAX_MANIFEST_BYTES);
109
141
  const archive = await fetchBuffer(`${baseUrl}/${filename}`, MAX_ARCHIVE_BYTES);
110
142
  verifyArchive(filename, archive, manifest.toString('utf8'));
111
- const executable = extractExecutable(archive, selected.executable);
143
+ const executables = extractExecutables(archive, [selected.executable, selected.resticExecutable]);
112
144
 
113
145
  const nativeDirectory = path.join(packageRoot, 'bin', 'native');
114
146
  const destination = path.join(nativeDirectory, selected.executable);
115
- const temporary = `${destination}.${process.pid}.tmp`;
116
- fs.mkdirSync(nativeDirectory, { recursive: true });
117
- try {
118
- fs.writeFileSync(temporary, executable, { mode: 0o755, flag: 'wx' });
119
- fs.renameSync(temporary, destination);
120
- if (process.platform !== 'win32') fs.chmodSync(destination, 0o755);
121
- } finally {
122
- fs.rmSync(temporary, { force: true });
123
- }
147
+ installExecutables(nativeDirectory, executables);
124
148
  const skillResults = installSkills({
125
149
  packageRoot,
126
150
  homeDirectory: options.homeDirectory,
@@ -136,7 +160,7 @@ module.exports = {
136
160
  RELEASE_BASE_URL,
137
161
  RELEASE_TAG_PREFIX,
138
162
  archiveName,
139
- extractExecutable,
163
+ extractExecutables,
140
164
  install,
141
165
  parseChecksumManifest,
142
166
  selectTarget,
@@ -58,43 +58,68 @@ function parseTarSize(header) {
58
58
  return Number.parseInt(value, 8);
59
59
  }
60
60
 
61
- function extractExecutable(archive, expectedName) {
62
- let tar;
61
+ function decompressArchive(archive) {
63
62
  try {
64
- tar = zlib.gunzipSync(archive);
63
+ return zlib.gunzipSync(archive);
65
64
  } catch (error) {
66
65
  throw new Error(`ARCHIVE_INVALID: cannot decompress release archive: ${error.message}`);
67
66
  }
67
+ }
68
+
69
+ function readTarEntry(tar, offset, expected, executables) {
70
+ const header = tar.subarray(offset, offset + 512);
71
+ if (header.every((byte) => byte === 0)) {
72
+ if (!tar.subarray(offset).every((byte) => byte === 0)) {
73
+ throw new Error('ARCHIVE_INVALID: unexpected data after tar terminator');
74
+ }
75
+ return null;
76
+ }
77
+ const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '');
78
+ const size = parseTarSize(header.subarray(124, 136));
79
+ const start = offset + 512;
80
+ const end = start + size;
81
+ if (end > tar.length) throw new Error('ARCHIVE_INVALID: truncated tar entry');
82
+ if (!expected.has(name) || ![0, '0'.charCodeAt(0)].includes(header[156])) {
83
+ throw new Error(`ARCHIVE_INVALID: unexpected archive entry ${name}`);
84
+ }
85
+ if (executables.has(name)) throw new Error(`ARCHIVE_INVALID: duplicate ${name}`);
86
+ executables.set(name, Buffer.from(tar.subarray(start, end)));
87
+ return start + Math.ceil(size / 512) * 512;
88
+ }
89
+
90
+ function requireExpectedExecutables(expected, executables) {
91
+ for (const name of expected) {
92
+ if (!executables.has(name)) {
93
+ throw new Error(`ARCHIVE_INVALID: archive does not contain ${name}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ function extractExecutables(archive, expectedNames) {
99
+ const expected = new Set(expectedNames);
100
+ if (expected.size !== expectedNames.length || expected.size === 0) {
101
+ throw new Error('ARCHIVE_INVALID: expected executable names must be unique');
102
+ }
103
+ const tar = decompressArchive(archive);
68
104
  let offset = 0;
69
- let executable = null;
105
+ let terminated = false;
106
+ const executables = new Map();
70
107
  while (offset + 512 <= tar.length) {
71
- const header = tar.subarray(offset, offset + 512);
72
- if (header.every((byte) => byte === 0)) {
73
- if (!tar.subarray(offset).every((byte) => byte === 0)) {
74
- throw new Error('ARCHIVE_INVALID: unexpected data after tar terminator');
75
- }
108
+ const next = readTarEntry(tar, offset, expected, executables);
109
+ if (next === null) {
110
+ terminated = true;
76
111
  break;
77
112
  }
78
- const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '');
79
- const size = parseTarSize(header.subarray(124, 136));
80
- const start = offset + 512;
81
- const end = start + size;
82
- if (end > tar.length) throw new Error('ARCHIVE_INVALID: truncated tar entry');
83
- if (name === expectedName) {
84
- if (executable) throw new Error(`ARCHIVE_INVALID: duplicate ${expectedName}`);
85
- executable = Buffer.from(tar.subarray(start, end));
86
- } else {
87
- throw new Error(`ARCHIVE_INVALID: unexpected archive entry ${name}`);
88
- }
89
- offset = start + Math.ceil(size / 512) * 512;
113
+ offset = next;
90
114
  }
91
- if (!executable) throw new Error(`ARCHIVE_INVALID: archive does not contain ${expectedName}`);
92
- return executable;
115
+ if (!terminated) throw new Error('ARCHIVE_INVALID: archive has no tar terminator');
116
+ requireExpectedExecutables(expected, executables);
117
+ return executables;
93
118
  }
94
119
 
95
120
  module.exports = {
96
121
  archiveName,
97
- extractExecutable,
122
+ extractExecutables,
98
123
  parseChecksumManifest,
99
124
  sha256,
100
125
  targetForHost,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine-company/zeroshot",
3
- "version": "10.8.0",
3
+ "version": "10.9.1",
4
4
  "description": "Verified prebuilt binary installer for the standalone Zeroshot engine",
5
5
  "homepage": "https://the-open-engine.github.io/zeroshot/",
6
6
  "license": "MIT",
@@ -52,6 +52,41 @@ zeroshot connection list --target cloud --scope org
52
52
  Do not assume a profile creates a pull request. `--pr` requests pull-request delivery and `--ship`
53
53
  requests merge delivery when materializing a compatible template; use either only when intended.
54
54
 
55
+ ## Prepare the execution environment
56
+
57
+ Environments belong to a run, independently of its profile. A `local:NAME` profile can run on a
58
+ Docker target with its own environment. For Docker or Cloud, pass
59
+ `--environment environment.json` with a flat definition such as:
60
+
61
+ ```json
62
+ {
63
+ "setup": "apt-get update && apt-get install -y jq",
64
+ "startup": "npm ci",
65
+ "variables": { "CI": "true" }
66
+ }
67
+ ```
68
+
69
+ Cloud resolves an omitted environment from the repository's User default, then Org default, then
70
+ Base. `--no-environment` bypasses defaults. Choose saved environments through Cloud's UI or API;
71
+ the CLI accepts a concrete JSON file. Local runs use the invoking machine and reject setup/startup
72
+ and hook connections.
73
+
74
+ Setup runs as root before checkout or restoration, so it cannot read repository files. Startup
75
+ runs as the workspace user in the checkout; workers and reviewers share its dependencies and
76
+ services. Both hooks rerun on resume, so startup must tolerate existing workspace files.
77
+ Checkpoints restore files and graph progress, not running services or agent conversations.
78
+
79
+ Put shared executables in `$ZEROSHOT_TOOLS/bin` and project dependencies in the workspace.
80
+ Shell exports do not persist between hooks or agents; use `variables` for nonsecret values and
81
+ `connections` for required secret field names. Hook connections and node connections are separate.
82
+ On a direct Docker target, root setup changes the container shared by its runs.
83
+
84
+ Preparation failures stop before agent execution without spending repair attempts. Inspect the
85
+ failed phase and logs; fix the environment instead of requesting unrelated code repairs. Resume
86
+ reuses the accepted definition, so an edited environment requires a new run. For a pinned Node
87
+ installation and service/Docker conventions, see the
88
+ [environment guide](https://the-open-engine.github.io/zeroshot/current/guides/runtime-environments/).
89
+
55
90
  ## Expose a profile over ACP
56
91
 
57
92
  Start `zeroshot acp` only when the user explicitly asks to expose a Zeroshot graph as an ACP agent.
package/targets.json CHANGED
@@ -3,30 +3,35 @@
3
3
  "platform": "linux",
4
4
  "arch": "x64",
5
5
  "target": "x86_64-unknown-linux-musl",
6
- "executable": "zeroshot"
6
+ "executable": "zeroshot",
7
+ "resticExecutable": "restic"
7
8
  },
8
9
  {
9
10
  "platform": "linux",
10
11
  "arch": "arm64",
11
12
  "target": "aarch64-unknown-linux-musl",
12
- "executable": "zeroshot"
13
+ "executable": "zeroshot",
14
+ "resticExecutable": "restic"
13
15
  },
14
16
  {
15
17
  "platform": "darwin",
16
18
  "arch": "x64",
17
19
  "target": "x86_64-apple-darwin",
18
- "executable": "zeroshot"
20
+ "executable": "zeroshot",
21
+ "resticExecutable": "restic"
19
22
  },
20
23
  {
21
24
  "platform": "darwin",
22
25
  "arch": "arm64",
23
26
  "target": "aarch64-apple-darwin",
24
- "executable": "zeroshot"
27
+ "executable": "zeroshot",
28
+ "resticExecutable": "restic"
25
29
  },
26
30
  {
27
31
  "platform": "win32",
28
32
  "arch": "x64",
29
33
  "target": "x86_64-pc-windows-msvc",
30
- "executable": "zeroshot.exe"
34
+ "executable": "zeroshot.exe",
35
+ "resticExecutable": "restic.exe"
31
36
  }
32
37
  ]