@holoscript/holosystem 0.1.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/LICENSE +21 -0
- package/README.md +105 -0
- package/bin/holosystem.mjs +173 -0
- package/package.json +59 -0
- package/src/index.mjs +574 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HoloScript ecosystem (Joseph, founder).
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @holoscript/holosystem
|
|
2
|
+
|
|
3
|
+
Create and inspect a portable HoloSystem consumer configuration from public npm
|
|
4
|
+
and PyPI package contracts. The package is for external founders, agent
|
|
5
|
+
operators, and cold consumers who need a small bootstrap contract without a
|
|
6
|
+
private workspace or machine-specific defaults.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @holoscript/holosystem
|
|
10
|
+
npx holosystem create --id acme-founder --workspace acme --json
|
|
11
|
+
npx holosystem inspect holosystem.config.json --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What It Creates
|
|
15
|
+
|
|
16
|
+
The default configuration pins public contracts for:
|
|
17
|
+
|
|
18
|
+
- `@holoscript/agent-runtime` for provider-neutral agent execution;
|
|
19
|
+
- `@holoscript/memory` for caller-owned memory;
|
|
20
|
+
- `@holoscript/holorepo` for JavaScript repository, database, and knowledge custody;
|
|
21
|
+
- `holoscript-holorepo` for the matching Python custody surface.
|
|
22
|
+
|
|
23
|
+
It also names caller-owned environment bindings for storage, memory, knowledge,
|
|
24
|
+
and authority. Only environment key names are stored. Credential values,
|
|
25
|
+
connection details, and private operator state are never configuration fields.
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { createHoloSystemConfig, inspectHoloSystemConfig } from '@holoscript/holosystem';
|
|
31
|
+
|
|
32
|
+
const config = createHoloSystemConfig({
|
|
33
|
+
consumerId: 'acme-founder',
|
|
34
|
+
workspace: 'acme',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const report = inspectHoloSystemConfig(config);
|
|
38
|
+
if (!report.ready) {
|
|
39
|
+
console.error(report.errors);
|
|
40
|
+
process.exitCode = 2;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`createHoloSystemConfig(options?)` returns deterministic JSON data. Callers may
|
|
45
|
+
supply their own exact public package contracts and caller-owned binding map.
|
|
46
|
+
The function fails closed when its result does not pass inspection.
|
|
47
|
+
|
|
48
|
+
`inspectHoloSystemConfig(config)` is pure and secret-safe. It reports named
|
|
49
|
+
checks for schema, identity, public contracts, caller bindings, portability,
|
|
50
|
+
bounded operations, and fresh receipt policy. Reports summarize package names,
|
|
51
|
+
versions, roles, and environment key names; they do not echo unknown config
|
|
52
|
+
values.
|
|
53
|
+
|
|
54
|
+
## CLI
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Write holosystem.config.json. Existing files require explicit --force.
|
|
58
|
+
npx holosystem create --id acme-founder --workspace acme
|
|
59
|
+
|
|
60
|
+
# Send the config to another agent without writing a file.
|
|
61
|
+
npx holosystem create --stdout
|
|
62
|
+
|
|
63
|
+
# Human-readable gate output.
|
|
64
|
+
npx holosystem inspect holosystem.config.json
|
|
65
|
+
|
|
66
|
+
# Machine-readable receipt. Exit code 2 means the config failed inspection.
|
|
67
|
+
npx holosystem inspect holosystem.config.json --json
|
|
68
|
+
|
|
69
|
+
# Pipeline input is supported.
|
|
70
|
+
npx holosystem create --stdout | npx holosystem inspect - --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Creation never installs packages, connects to storage, acquires credentials, or
|
|
74
|
+
mutates services. Those actions belong to the selected public packages and the
|
|
75
|
+
operator's own adapters. The CLI only writes the requested configuration file;
|
|
76
|
+
inspection is read-only.
|
|
77
|
+
|
|
78
|
+
## Consumption Contract
|
|
79
|
+
|
|
80
|
+
Consumers bring their own configuration, credentials, Postgres storage,
|
|
81
|
+
knowledge and authority adapters, receipt sink, and runtime host. Environment
|
|
82
|
+
variables reference that caller-owned state. The package does not ship or
|
|
83
|
+
assume founder-local paths, private repositories, private process, a specific
|
|
84
|
+
machine, or a hosted provider.
|
|
85
|
+
|
|
86
|
+
The portability gate rejects:
|
|
87
|
+
|
|
88
|
+
- local, Git, URL, or floating package specs instead of pinned registry releases;
|
|
89
|
+
- missing npm or PyPI custody roles;
|
|
90
|
+
- absolute receipt paths or parent traversal;
|
|
91
|
+
- embedded password, secret, token, private-key, or connection-string fields;
|
|
92
|
+
- non-caller binding ownership;
|
|
93
|
+
- unbounded autonomy, missing stop conditions, or stale receipt policy.
|
|
94
|
+
|
|
95
|
+
Agent-operable evidence is the `inspect --json` receipt. Before release, run the
|
|
96
|
+
package check, an npm pack dry run, and the repository public-consumption gate.
|
|
97
|
+
|
|
98
|
+
## Release Boundary
|
|
99
|
+
|
|
100
|
+
Release lane: `v0-preview`. Supported behavior is deterministic config creation
|
|
101
|
+
and static, secret-safe portability inspection for the documented v1 schema.
|
|
102
|
+
Known limitations: this package does not verify registry availability, install
|
|
103
|
+
dependencies, test live credentials, bootstrap databases, or authorize agent
|
|
104
|
+
work. Roll back by pinning the previous package version and retaining the
|
|
105
|
+
consumer-owned config and receipts.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { createHoloSystemConfig, inspectHoloSystemConfig } from '../src/index.mjs';
|
|
6
|
+
|
|
7
|
+
const CLI_RECEIPT_SCHEMA = 'holoscript.holosystem.cli-receipt.v1';
|
|
8
|
+
const HELP = `holosystem - create or inspect a portable HoloSystem consumer config
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
holosystem create [--id <id>] [--workspace <id>] [--output <file>] [--force] [--json]
|
|
12
|
+
holosystem create --stdout
|
|
13
|
+
holosystem inspect [file|-] [--json]
|
|
14
|
+
holosystem --help
|
|
15
|
+
holosystem --version
|
|
16
|
+
|
|
17
|
+
Defaults:
|
|
18
|
+
create writes holosystem.config.json and never overwrites it without --force.
|
|
19
|
+
inspect reads holosystem.config.json. Use - to read JSON from stdin.
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
function die(message, { json = false, code = 1 } = {}) {
|
|
23
|
+
if (json) {
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`${JSON.stringify({ schema: CLI_RECEIPT_SCHEMA, ok: false, error: message }, null, 2)}\n`
|
|
26
|
+
);
|
|
27
|
+
} else {
|
|
28
|
+
process.stderr.write(`[holosystem] ${message}\n`);
|
|
29
|
+
}
|
|
30
|
+
process.exit(code);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseArguments(args, specification) {
|
|
34
|
+
const options = {};
|
|
35
|
+
const positionals = [];
|
|
36
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
37
|
+
const item = args[index];
|
|
38
|
+
if (!item.startsWith('--')) {
|
|
39
|
+
positionals.push(item);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const equals = item.indexOf('=');
|
|
43
|
+
const name = item.slice(2, equals === -1 ? undefined : equals);
|
|
44
|
+
const type = specification[name];
|
|
45
|
+
if (!type) throw new Error(`Unknown option --${name}.`);
|
|
46
|
+
if (type === 'boolean') {
|
|
47
|
+
if (equals !== -1) throw new Error(`Option --${name} does not accept a value.`);
|
|
48
|
+
options[name] = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const value = equals === -1 ? args[index + 1] : item.slice(equals + 1);
|
|
52
|
+
if (!value || value.startsWith('--')) throw new Error(`Option --${name} requires a value.`);
|
|
53
|
+
options[name] = value;
|
|
54
|
+
if (equals === -1) index += 1;
|
|
55
|
+
}
|
|
56
|
+
return { options, positionals };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function receipt(operation, fields) {
|
|
60
|
+
return {
|
|
61
|
+
schema: CLI_RECEIPT_SCHEMA,
|
|
62
|
+
generatedAt: new Date().toISOString(),
|
|
63
|
+
operation,
|
|
64
|
+
...fields,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function outputJson(value) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runCreate(args) {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = parseArguments(args, {
|
|
76
|
+
id: 'value',
|
|
77
|
+
workspace: 'value',
|
|
78
|
+
output: 'value',
|
|
79
|
+
force: 'boolean',
|
|
80
|
+
stdout: 'boolean',
|
|
81
|
+
json: 'boolean',
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
die(error.message, { json: args.includes('--json') });
|
|
85
|
+
}
|
|
86
|
+
const { options, positionals } = parsed;
|
|
87
|
+
if (positionals.length > 0)
|
|
88
|
+
die('create does not accept positional arguments.', { json: options.json });
|
|
89
|
+
if (options.stdout && options.output) {
|
|
90
|
+
die('--stdout and --output cannot be combined.', { json: options.json });
|
|
91
|
+
}
|
|
92
|
+
if (options.stdout && options.json) {
|
|
93
|
+
die('--stdout already emits JSON config and cannot be combined with --json.', { json: true });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let config;
|
|
97
|
+
try {
|
|
98
|
+
config = createHoloSystemConfig({
|
|
99
|
+
consumerId: options.id || 'external-founder',
|
|
100
|
+
workspace: options.workspace || 'default',
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
die(error.message, { json: options.json, code: 2 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options.stdout) {
|
|
107
|
+
outputJson(config);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const output = options.output || 'holosystem.config.json';
|
|
112
|
+
const absoluteOutput = resolve(process.cwd(), output);
|
|
113
|
+
if (existsSync(absoluteOutput) && !options.force) {
|
|
114
|
+
die(`${output} already exists; use --force to replace it.`, { json: options.json });
|
|
115
|
+
}
|
|
116
|
+
writeFileSync(absoluteOutput, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
117
|
+
const report = inspectHoloSystemConfig(config);
|
|
118
|
+
const result = receipt('create', { ok: report.ready, output, report });
|
|
119
|
+
if (options.json) outputJson(result);
|
|
120
|
+
else process.stdout.write(`Created ${output}\nPortable: ${report.portable ? 'yes' : 'no'}\n`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readStdin() {
|
|
124
|
+
return readFileSync(0, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runInspect(args) {
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = parseArguments(args, { json: 'boolean' });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
die(error.message, { json: args.includes('--json') });
|
|
133
|
+
}
|
|
134
|
+
const { options, positionals } = parsed;
|
|
135
|
+
if (positionals.length > 1) die('inspect accepts at most one file.', { json: options.json });
|
|
136
|
+
const input = positionals[0] || 'holosystem.config.json';
|
|
137
|
+
let config;
|
|
138
|
+
try {
|
|
139
|
+
const source =
|
|
140
|
+
input === '-' ? readStdin() : readFileSync(resolve(process.cwd(), input), 'utf8');
|
|
141
|
+
config = JSON.parse(source.replace(/^\uFEFF/u, ''));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
die(`Cannot read ${input}: ${error.message}`, { json: options.json });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const report = inspectHoloSystemConfig(config);
|
|
147
|
+
if (options.json) outputJson(receipt('inspect', { ok: report.ready, input, report }));
|
|
148
|
+
else {
|
|
149
|
+
process.stdout.write(
|
|
150
|
+
`Ready: ${report.ready ? 'yes' : 'no'}\nPortable: ${report.portable ? 'yes' : 'no'}\n`
|
|
151
|
+
);
|
|
152
|
+
for (const check of report.checks) {
|
|
153
|
+
process.stdout.write(`${check.ok ? 'PASS' : 'FAIL'} ${check.id}\n`);
|
|
154
|
+
}
|
|
155
|
+
for (const error of report.errors) process.stdout.write(` ${error.path}: ${error.message}\n`);
|
|
156
|
+
}
|
|
157
|
+
if (!report.ready) process.exitCode = 2;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const argv = process.argv.slice(2);
|
|
161
|
+
const command = argv[0];
|
|
162
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
163
|
+
process.stdout.write(HELP);
|
|
164
|
+
} else if (command === '--version' || command === '-v') {
|
|
165
|
+
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
166
|
+
process.stdout.write(`${manifest.version}\n`);
|
|
167
|
+
} else if (command === 'create') {
|
|
168
|
+
runCreate(argv.slice(1));
|
|
169
|
+
} else if (command === 'inspect') {
|
|
170
|
+
runInspect(argv.slice(1));
|
|
171
|
+
} else {
|
|
172
|
+
die(`Unknown command ${command}. Run holosystem --help.`);
|
|
173
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@holoscript/holosystem",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Portable public-package bootstrap and inspection contract for HoloSystem consumers.",
|
|
5
|
+
"releaseLane": "v0-preview",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"holoscript",
|
|
8
|
+
"holosystem",
|
|
9
|
+
"agent-runtime",
|
|
10
|
+
"sovereign",
|
|
11
|
+
"bootstrap",
|
|
12
|
+
"portable-config"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/brianonbased-dev/HoloScript.git",
|
|
18
|
+
"directory": "packages/holosystem"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/brianonbased-dev/HoloScript/tree/main/packages/holosystem",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/brianonbased-dev/HoloScript/issues"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"holosystem": "./bin/holosystem.mjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node --check src/index.mjs && node --check bin/holosystem.mjs",
|
|
45
|
+
"test": "node --test test/holosystem.test.mjs test/cli.test.mjs",
|
|
46
|
+
"smoke": "node bin/holosystem.mjs --help",
|
|
47
|
+
"check": "pnpm run build && pnpm run test && pnpm run smoke",
|
|
48
|
+
"prepublishOnly": "pnpm run check"
|
|
49
|
+
},
|
|
50
|
+
"holorepo": {
|
|
51
|
+
"releaseLane": "v0-preview",
|
|
52
|
+
"pypiCompanion": "holoscript-holorepo",
|
|
53
|
+
"publicConsumption": {
|
|
54
|
+
"coreRule": "Cold consumers create and inspect a HoloSystem configuration from public registry contracts and caller-owned bindings.",
|
|
55
|
+
"localBoundary": "Machine paths, credentials, private repositories, and operator state are caller inputs, never package defaults.",
|
|
56
|
+
"supportBoundary": "Configuration creation and static portability inspection are supported; installation, credential acquisition, service mutation, and publishing are outside this package."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
export const HOLOSYSTEM_CONFIG_SCHEMA = 'holoscript.holosystem.consumer.v1';
|
|
2
|
+
export const HOLOSYSTEM_INSPECTION_SCHEMA = 'holoscript.holosystem.inspection.v1';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_CONTRACTS = [
|
|
5
|
+
{ registry: 'npm', role: 'agent-runtime' },
|
|
6
|
+
{ registry: 'npm', role: 'memory' },
|
|
7
|
+
{ registry: 'npm', role: 'repo-custody' },
|
|
8
|
+
{ registry: 'pypi', role: 'repo-custody' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const REQUIRED_BINDINGS = ['storage', 'memory', 'knowledge', 'authority'];
|
|
12
|
+
const REQUIRED_STOP_CONDITIONS = ['authority-required', 'validation-failed', 'lease-expired'];
|
|
13
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set(['schema', 'consumer', 'contracts', 'bindings', 'operations']);
|
|
14
|
+
|
|
15
|
+
function deepFreeze(value) {
|
|
16
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
17
|
+
Object.freeze(value);
|
|
18
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clone(value) {
|
|
23
|
+
return JSON.parse(JSON.stringify(value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_PUBLIC_PACKAGE_CONTRACTS = deepFreeze([
|
|
27
|
+
{
|
|
28
|
+
registry: 'npm',
|
|
29
|
+
name: '@holoscript/agent-runtime',
|
|
30
|
+
version: '0.6.1',
|
|
31
|
+
role: 'agent-runtime',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
registry: 'npm',
|
|
35
|
+
name: '@holoscript/memory',
|
|
36
|
+
version: '0.3.0',
|
|
37
|
+
role: 'memory',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
registry: 'npm',
|
|
41
|
+
name: '@holoscript/holorepo',
|
|
42
|
+
version: '0.3.7',
|
|
43
|
+
role: 'repo-custody',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
registry: 'pypi',
|
|
47
|
+
name: 'holoscript-holorepo',
|
|
48
|
+
version: '0.3.7',
|
|
49
|
+
role: 'repo-custody',
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_CALLER_BINDINGS = deepFreeze({
|
|
54
|
+
storage: { owner: 'caller', env: 'HOLOREPO_DATABASE_URL' },
|
|
55
|
+
memory: { owner: 'caller', env: 'MEMORY_DATABASE_URL' },
|
|
56
|
+
knowledge: { owner: 'caller', env: 'HOLOREPO_DATABASE_URL' },
|
|
57
|
+
authority: { owner: 'caller', env: 'HOLOSYSTEM_AUTHORITY_REF' },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const DEFAULT_OPERATIONS = deepFreeze({
|
|
61
|
+
autonomy: {
|
|
62
|
+
mode: 'bounded',
|
|
63
|
+
maxAttempts: 3,
|
|
64
|
+
leaseSeconds: 300,
|
|
65
|
+
stopConditions: REQUIRED_STOP_CONDITIONS,
|
|
66
|
+
},
|
|
67
|
+
receipts: {
|
|
68
|
+
directory: '.holosystem/receipts',
|
|
69
|
+
format: 'json',
|
|
70
|
+
maxAgeSeconds: 900,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function issue(list, check, code, path, message) {
|
|
75
|
+
list.push({ check, code, path, message });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkKnownKeys(value, allowed, path, check, errors) {
|
|
79
|
+
if (!isRecord(value)) return;
|
|
80
|
+
for (const key of Object.keys(value)) {
|
|
81
|
+
if (!allowed.has(key)) {
|
|
82
|
+
issue(
|
|
83
|
+
errors,
|
|
84
|
+
check,
|
|
85
|
+
'unknown-field',
|
|
86
|
+
path ? `${path}.${key}` : key,
|
|
87
|
+
`Field ${key} is not part of the v1 consumer contract.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRecord(value) {
|
|
94
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validId(value) {
|
|
98
|
+
return typeof value === 'string' && /^[a-z0-9][a-z0-9._-]{0,63}$/iu.test(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validEnvName(value) {
|
|
102
|
+
return typeof value === 'string' && /^[A-Z][A-Z0-9_]*$/u.test(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validPackageName(registry, value) {
|
|
106
|
+
if (typeof value !== 'string') return false;
|
|
107
|
+
if (registry === 'npm') {
|
|
108
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u.test(value);
|
|
109
|
+
}
|
|
110
|
+
return registry === 'pypi' && /^[a-z0-9]+(?:[-_.][a-z0-9]+)*$/iu.test(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validPinnedVersion(value) {
|
|
114
|
+
return (
|
|
115
|
+
typeof value === 'string' &&
|
|
116
|
+
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(value) &&
|
|
117
|
+
!/^(?:workspace|file|link|portal|git|https?):/iu.test(value)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isPortablePath(value) {
|
|
122
|
+
return (
|
|
123
|
+
typeof value === 'string' &&
|
|
124
|
+
value.length > 0 &&
|
|
125
|
+
!/^[A-Za-z]:[\\/]/u.test(value) &&
|
|
126
|
+
!/^(?:[\\/]{2}|\/|~[\\/])/u.test(value) &&
|
|
127
|
+
!/(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(value)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function inspectSensitiveValues(value, path, errors) {
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
value.forEach((child, index) => inspectSensitiveValues(child, `${path}[${index}]`, errors));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!isRecord(value)) return;
|
|
137
|
+
|
|
138
|
+
for (const [key, child] of Object.entries(value)) {
|
|
139
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
140
|
+
if (/(?:password|secret|token|apiKey|privateKey|connectionString)$/iu.test(key)) {
|
|
141
|
+
issue(
|
|
142
|
+
errors,
|
|
143
|
+
'portable-inputs',
|
|
144
|
+
'embedded-sensitive-field',
|
|
145
|
+
childPath,
|
|
146
|
+
'Credential values are not configuration fields; reference a caller-owned environment key.'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
inspectSensitiveValues(child, childPath, errors);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkSchema(config, errors) {
|
|
154
|
+
if (config.schema !== HOLOSYSTEM_CONFIG_SCHEMA) {
|
|
155
|
+
issue(errors, 'schema', 'schema-mismatch', 'schema', `Expected ${HOLOSYSTEM_CONFIG_SCHEMA}.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function checkConsumer(config, errors) {
|
|
160
|
+
if (!isRecord(config.consumer)) {
|
|
161
|
+
issue(errors, 'consumer', 'consumer-missing', 'consumer', 'Consumer identity is required.');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
checkKnownKeys(config.consumer, new Set(['id', 'workspace']), 'consumer', 'consumer', errors);
|
|
165
|
+
if (!validId(config.consumer.id)) {
|
|
166
|
+
issue(
|
|
167
|
+
errors,
|
|
168
|
+
'consumer',
|
|
169
|
+
'consumer-id-invalid',
|
|
170
|
+
'consumer.id',
|
|
171
|
+
'Consumer id must be a portable identifier of 1 to 64 characters.'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (!validId(config.consumer.workspace)) {
|
|
175
|
+
issue(
|
|
176
|
+
errors,
|
|
177
|
+
'consumer',
|
|
178
|
+
'workspace-id-invalid',
|
|
179
|
+
'consumer.workspace',
|
|
180
|
+
'Workspace must be a portable identifier of 1 to 64 characters.'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function checkContracts(config, errors) {
|
|
186
|
+
if (!Array.isArray(config.contracts)) {
|
|
187
|
+
issue(
|
|
188
|
+
errors,
|
|
189
|
+
'public-contracts',
|
|
190
|
+
'contracts-missing',
|
|
191
|
+
'contracts',
|
|
192
|
+
'Public package contracts must be an array.'
|
|
193
|
+
);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const contracts = [];
|
|
198
|
+
const identities = new Set();
|
|
199
|
+
config.contracts.forEach((contract, index) => {
|
|
200
|
+
const path = `contracts[${index}]`;
|
|
201
|
+
if (!isRecord(contract)) {
|
|
202
|
+
issue(errors, 'public-contracts', 'contract-invalid', path, 'Contract must be an object.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
checkKnownKeys(
|
|
206
|
+
contract,
|
|
207
|
+
new Set(['registry', 'name', 'version', 'role']),
|
|
208
|
+
path,
|
|
209
|
+
'public-contracts',
|
|
210
|
+
errors
|
|
211
|
+
);
|
|
212
|
+
const registry = contract.registry;
|
|
213
|
+
if (registry !== 'npm' && registry !== 'pypi') {
|
|
214
|
+
issue(
|
|
215
|
+
errors,
|
|
216
|
+
'public-contracts',
|
|
217
|
+
'registry-invalid',
|
|
218
|
+
`${path}.registry`,
|
|
219
|
+
'Registry must be npm or pypi.'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (!validPackageName(registry, contract.name)) {
|
|
223
|
+
issue(
|
|
224
|
+
errors,
|
|
225
|
+
'public-contracts',
|
|
226
|
+
'package-name-invalid',
|
|
227
|
+
`${path}.name`,
|
|
228
|
+
'Package name is invalid for its public registry.'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (!validPinnedVersion(contract.version)) {
|
|
232
|
+
issue(
|
|
233
|
+
errors,
|
|
234
|
+
'public-contracts',
|
|
235
|
+
'version-not-pinned',
|
|
236
|
+
`${path}.version`,
|
|
237
|
+
'Version must be an exact public release, not a local, remote, or floating spec.'
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (!validId(contract.role)) {
|
|
241
|
+
issue(
|
|
242
|
+
errors,
|
|
243
|
+
'public-contracts',
|
|
244
|
+
'role-invalid',
|
|
245
|
+
`${path}.role`,
|
|
246
|
+
'Role must be a portable identifier.'
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const identity = `${registry}:${String(contract.name).toLowerCase()}`;
|
|
250
|
+
if (identities.has(identity)) {
|
|
251
|
+
issue(
|
|
252
|
+
errors,
|
|
253
|
+
'public-contracts',
|
|
254
|
+
'contract-duplicate',
|
|
255
|
+
path,
|
|
256
|
+
`Duplicate public package contract ${identity}.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
identities.add(identity);
|
|
260
|
+
contracts.push({
|
|
261
|
+
registry,
|
|
262
|
+
name: typeof contract.name === 'string' ? contract.name : null,
|
|
263
|
+
version: typeof contract.version === 'string' ? contract.version : null,
|
|
264
|
+
role: typeof contract.role === 'string' ? contract.role : null,
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
for (const required of REQUIRED_CONTRACTS) {
|
|
269
|
+
if (
|
|
270
|
+
!contracts.some(
|
|
271
|
+
(contract) => contract.registry === required.registry && contract.role === required.role
|
|
272
|
+
)
|
|
273
|
+
) {
|
|
274
|
+
issue(
|
|
275
|
+
errors,
|
|
276
|
+
'public-contracts',
|
|
277
|
+
'required-contract-missing',
|
|
278
|
+
'contracts',
|
|
279
|
+
`Missing ${required.registry} contract for role ${required.role}.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return contracts;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function checkBindings(config, errors) {
|
|
287
|
+
if (!isRecord(config.bindings)) {
|
|
288
|
+
issue(
|
|
289
|
+
errors,
|
|
290
|
+
'caller-bindings',
|
|
291
|
+
'bindings-missing',
|
|
292
|
+
'bindings',
|
|
293
|
+
'Caller-owned bindings are required.'
|
|
294
|
+
);
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const bindings = [];
|
|
299
|
+
for (const id of REQUIRED_BINDINGS) {
|
|
300
|
+
if (!isRecord(config.bindings[id])) {
|
|
301
|
+
issue(
|
|
302
|
+
errors,
|
|
303
|
+
'caller-bindings',
|
|
304
|
+
'binding-missing',
|
|
305
|
+
`bindings.${id}`,
|
|
306
|
+
`Missing ${id} binding.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const [id, binding] of Object.entries(config.bindings)) {
|
|
312
|
+
const path = `bindings.${id}`;
|
|
313
|
+
if (!validId(id)) {
|
|
314
|
+
issue(
|
|
315
|
+
errors,
|
|
316
|
+
'caller-bindings',
|
|
317
|
+
'binding-id-invalid',
|
|
318
|
+
path,
|
|
319
|
+
'Binding id must be a portable identifier.'
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (!isRecord(binding)) {
|
|
323
|
+
issue(errors, 'caller-bindings', 'binding-invalid', path, 'Binding must be an object.');
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
checkKnownKeys(binding, new Set(['owner', 'env']), path, 'caller-bindings', errors);
|
|
327
|
+
if (binding.owner !== 'caller') {
|
|
328
|
+
issue(
|
|
329
|
+
errors,
|
|
330
|
+
'caller-bindings',
|
|
331
|
+
'binding-owner-invalid',
|
|
332
|
+
`${path}.owner`,
|
|
333
|
+
'Binding ownership must remain with the caller.'
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (!validEnvName(binding.env)) {
|
|
337
|
+
issue(
|
|
338
|
+
errors,
|
|
339
|
+
'caller-bindings',
|
|
340
|
+
'binding-env-invalid',
|
|
341
|
+
`${path}.env`,
|
|
342
|
+
'Binding must name an environment key without embedding its value.'
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
bindings.push({ id, owner: binding.owner, env: binding.env });
|
|
346
|
+
}
|
|
347
|
+
return bindings;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function checkOperations(config, errors) {
|
|
351
|
+
const operations = config.operations;
|
|
352
|
+
if (!isRecord(operations)) {
|
|
353
|
+
issue(
|
|
354
|
+
errors,
|
|
355
|
+
'bounded-operations',
|
|
356
|
+
'operations-missing',
|
|
357
|
+
'operations',
|
|
358
|
+
'Bounded autonomy and receipt policy are required.'
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
checkKnownKeys(
|
|
363
|
+
operations,
|
|
364
|
+
new Set(['autonomy', 'receipts']),
|
|
365
|
+
'operations',
|
|
366
|
+
'bounded-operations',
|
|
367
|
+
errors
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const autonomy = operations.autonomy;
|
|
371
|
+
if (!isRecord(autonomy)) {
|
|
372
|
+
issue(
|
|
373
|
+
errors,
|
|
374
|
+
'bounded-operations',
|
|
375
|
+
'autonomy-missing',
|
|
376
|
+
'operations.autonomy',
|
|
377
|
+
'Autonomy policy is required.'
|
|
378
|
+
);
|
|
379
|
+
} else {
|
|
380
|
+
checkKnownKeys(
|
|
381
|
+
autonomy,
|
|
382
|
+
new Set(['mode', 'maxAttempts', 'leaseSeconds', 'stopConditions']),
|
|
383
|
+
'operations.autonomy',
|
|
384
|
+
'bounded-operations',
|
|
385
|
+
errors
|
|
386
|
+
);
|
|
387
|
+
if (autonomy.mode !== 'bounded') {
|
|
388
|
+
issue(
|
|
389
|
+
errors,
|
|
390
|
+
'bounded-operations',
|
|
391
|
+
'autonomy-unbounded',
|
|
392
|
+
'operations.autonomy.mode',
|
|
393
|
+
'Autonomy mode must be bounded.'
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (!Number.isInteger(autonomy.maxAttempts) || autonomy.maxAttempts < 1) {
|
|
397
|
+
issue(
|
|
398
|
+
errors,
|
|
399
|
+
'bounded-operations',
|
|
400
|
+
'max-attempts-invalid',
|
|
401
|
+
'operations.autonomy.maxAttempts',
|
|
402
|
+
'maxAttempts must be a positive integer.'
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (!Number.isInteger(autonomy.leaseSeconds) || autonomy.leaseSeconds < 1) {
|
|
406
|
+
issue(
|
|
407
|
+
errors,
|
|
408
|
+
'bounded-operations',
|
|
409
|
+
'lease-invalid',
|
|
410
|
+
'operations.autonomy.leaseSeconds',
|
|
411
|
+
'leaseSeconds must be a positive integer.'
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
const stops = Array.isArray(autonomy.stopConditions) ? autonomy.stopConditions : [];
|
|
415
|
+
for (const stop of REQUIRED_STOP_CONDITIONS) {
|
|
416
|
+
if (!stops.includes(stop)) {
|
|
417
|
+
issue(
|
|
418
|
+
errors,
|
|
419
|
+
'bounded-operations',
|
|
420
|
+
'stop-condition-missing',
|
|
421
|
+
'operations.autonomy.stopConditions',
|
|
422
|
+
`Missing stop condition ${stop}.`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const receipts = operations.receipts;
|
|
429
|
+
if (!isRecord(receipts)) {
|
|
430
|
+
issue(
|
|
431
|
+
errors,
|
|
432
|
+
'bounded-operations',
|
|
433
|
+
'receipts-missing',
|
|
434
|
+
'operations.receipts',
|
|
435
|
+
'Receipt policy is required.'
|
|
436
|
+
);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
checkKnownKeys(
|
|
440
|
+
receipts,
|
|
441
|
+
new Set(['directory', 'format', 'maxAgeSeconds']),
|
|
442
|
+
'operations.receipts',
|
|
443
|
+
'bounded-operations',
|
|
444
|
+
errors
|
|
445
|
+
);
|
|
446
|
+
if (!isPortablePath(receipts.directory)) {
|
|
447
|
+
issue(
|
|
448
|
+
errors,
|
|
449
|
+
'portable-inputs',
|
|
450
|
+
'receipt-path-not-portable',
|
|
451
|
+
'operations.receipts.directory',
|
|
452
|
+
'Receipt directory must be a relative path without parent traversal.'
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (receipts.format !== 'json') {
|
|
456
|
+
issue(
|
|
457
|
+
errors,
|
|
458
|
+
'bounded-operations',
|
|
459
|
+
'receipt-format-invalid',
|
|
460
|
+
'operations.receipts.format',
|
|
461
|
+
'Receipt format must be json.'
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (!Number.isInteger(receipts.maxAgeSeconds) || receipts.maxAgeSeconds < 1) {
|
|
465
|
+
issue(
|
|
466
|
+
errors,
|
|
467
|
+
'bounded-operations',
|
|
468
|
+
'receipt-freshness-invalid',
|
|
469
|
+
'operations.receipts.maxAgeSeconds',
|
|
470
|
+
'maxAgeSeconds must be a positive integer.'
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function inspectHoloSystemConfig(config) {
|
|
476
|
+
const errors = [];
|
|
477
|
+
const warnings = [];
|
|
478
|
+
if (!isRecord(config)) {
|
|
479
|
+
issue(errors, 'schema', 'config-invalid', '$', 'Configuration must be a JSON object.');
|
|
480
|
+
return {
|
|
481
|
+
schema: HOLOSYSTEM_INSPECTION_SCHEMA,
|
|
482
|
+
ready: false,
|
|
483
|
+
portable: false,
|
|
484
|
+
checks: [{ id: 'schema', ok: false, errorCount: 1 }],
|
|
485
|
+
errors,
|
|
486
|
+
warnings,
|
|
487
|
+
summary: { consumer: null, packages: [], bindings: [] },
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
checkSchema(config, errors);
|
|
492
|
+
checkConsumer(config, errors);
|
|
493
|
+
const packages = checkContracts(config, errors);
|
|
494
|
+
const bindings = checkBindings(config, errors);
|
|
495
|
+
checkOperations(config, errors);
|
|
496
|
+
inspectSensitiveValues(config, '', errors);
|
|
497
|
+
checkKnownKeys(config, KNOWN_TOP_LEVEL_KEYS, '', 'schema', errors);
|
|
498
|
+
|
|
499
|
+
const checkIds = [
|
|
500
|
+
'schema',
|
|
501
|
+
'consumer',
|
|
502
|
+
'public-contracts',
|
|
503
|
+
'caller-bindings',
|
|
504
|
+
'portable-inputs',
|
|
505
|
+
'bounded-operations',
|
|
506
|
+
];
|
|
507
|
+
const checks = checkIds.map((id) => {
|
|
508
|
+
const errorCount = errors.filter((error) => error.check === id).length;
|
|
509
|
+
return { id, ok: errorCount === 0, errorCount };
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
schema: HOLOSYSTEM_INSPECTION_SCHEMA,
|
|
514
|
+
ready: errors.length === 0,
|
|
515
|
+
portable: errors.every(
|
|
516
|
+
(error) =>
|
|
517
|
+
!['schema', 'consumer', 'public-contracts', 'caller-bindings', 'portable-inputs'].includes(
|
|
518
|
+
error.check
|
|
519
|
+
)
|
|
520
|
+
),
|
|
521
|
+
checks,
|
|
522
|
+
errors,
|
|
523
|
+
warnings,
|
|
524
|
+
summary: {
|
|
525
|
+
consumer: {
|
|
526
|
+
id: typeof config.consumer?.id === 'string' ? config.consumer.id : null,
|
|
527
|
+
workspace:
|
|
528
|
+
typeof config.consumer?.workspace === 'string' ? config.consumer.workspace : null,
|
|
529
|
+
},
|
|
530
|
+
packages,
|
|
531
|
+
bindings,
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function createHoloSystemConfig({
|
|
537
|
+
consumerId = 'external-founder',
|
|
538
|
+
workspace = 'default',
|
|
539
|
+
contracts = DEFAULT_PUBLIC_PACKAGE_CONTRACTS,
|
|
540
|
+
bindings = DEFAULT_CALLER_BINDINGS,
|
|
541
|
+
receiptDirectory = DEFAULT_OPERATIONS.receipts.directory,
|
|
542
|
+
maxAttempts = DEFAULT_OPERATIONS.autonomy.maxAttempts,
|
|
543
|
+
leaseSeconds = DEFAULT_OPERATIONS.autonomy.leaseSeconds,
|
|
544
|
+
receiptMaxAgeSeconds = DEFAULT_OPERATIONS.receipts.maxAgeSeconds,
|
|
545
|
+
} = {}) {
|
|
546
|
+
const config = {
|
|
547
|
+
schema: HOLOSYSTEM_CONFIG_SCHEMA,
|
|
548
|
+
consumer: { id: consumerId, workspace },
|
|
549
|
+
contracts: clone(contracts),
|
|
550
|
+
bindings: clone(bindings),
|
|
551
|
+
operations: {
|
|
552
|
+
autonomy: {
|
|
553
|
+
mode: 'bounded',
|
|
554
|
+
maxAttempts,
|
|
555
|
+
leaseSeconds,
|
|
556
|
+
stopConditions: clone(REQUIRED_STOP_CONDITIONS),
|
|
557
|
+
},
|
|
558
|
+
receipts: {
|
|
559
|
+
directory: receiptDirectory,
|
|
560
|
+
format: 'json',
|
|
561
|
+
maxAgeSeconds: receiptMaxAgeSeconds,
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
const report = inspectHoloSystemConfig(config);
|
|
566
|
+
if (!report.ready) {
|
|
567
|
+
const error = new TypeError(
|
|
568
|
+
`Cannot create HoloSystem config: ${report.errors.map((item) => item.code).join(', ')}`
|
|
569
|
+
);
|
|
570
|
+
error.report = report;
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
return config;
|
|
574
|
+
}
|