ace-runtime 0.1.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +9 -0
- data/docs/usage.md +123 -0
- data/lib/ace/runtime/molecules/cgroup_observation.rb +148 -0
- data/lib/ace/runtime/molecules/codex_runtime_installation.rb +130 -0
- data/lib/ace/runtime/molecules/execution_boot_baseline.rb +134 -0
- data/lib/ace/runtime/molecules/execution_network_selection.rb +85 -0
- data/lib/ace/runtime/molecules/execution_unit_installation.rb +663 -0
- data/lib/ace/runtime/molecules/kernel_view_topology.rb +133 -0
- data/lib/ace/runtime/molecules/linux_mount_info.rb +101 -0
- data/lib/ace/runtime/molecules/network_installation_evidence.rb +332 -0
- data/lib/ace/runtime/molecules/process_identity.rb +123 -0
- data/lib/ace/runtime/molecules/protected_artifact_set.rb +211 -0
- data/lib/ace/runtime/molecules/protected_linux.rb +118 -0
- data/lib/ace/runtime/molecules/protected_socket.rb +119 -0
- data/lib/ace/runtime/molecules/protected_task_context_entry.rb +162 -0
- data/lib/ace/runtime/molecules/protected_worker_entry.rb +32 -0
- data/lib/ace/runtime/molecules/readiness_configuration.rb +154 -0
- data/lib/ace/runtime/molecules/readiness_runtime.rb +94 -0
- data/lib/ace/runtime/molecules/server_resource_observation.rb +183 -0
- data/lib/ace/runtime/molecules/systemd_scope_manager.rb +411 -0
- data/lib/ace/runtime/testing/adapter_contract.rb +9 -0
- data/lib/ace/runtime/version.rb +1 -1
- data/lib/ace/runtime.rb +3 -0
- data/native/README.md +124 -0
- data/native/build-worker-gate +20 -0
- data/native/worker_gate.c +385 -0
- metadata +63 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a5a447921ab8d6b478e41a38e6b56866977accc8a75250e394c89c6fa0ac658
|
|
4
|
+
data.tar.gz: efe75a6544cb3284a9bf544deaaa30975b60971a5c821799666502240442ec7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46343d278d6041fcd7f41c859207ee0303e28811d156b923bc195b88cf73aef852d3f44b50df62a90425f0cec8a942984504ca9a7c52e4cd9152ffc77679d87a
|
|
7
|
+
data.tar.gz: b61c518fb64562ddb1975be5b6b71242134f013ec80de029bf259c5d92acdd2572b428f60788e4e4578e7f5f2a59abb3e50ec8664eaa462ceb65482ce3b6857e
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,64 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
- Ship the protected task-context/worker entry and readiness source used by the current consumers; these required modules were absent from published 0.2.0. Ordinary agent execution remains the ace-llm terminal CLI, with no additional Codex service.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
- Pin and type-check the original native server's network namespace throughout readiness observation; refuse namespace replacement before returning its identity.
|
|
15
|
+
- Verify dedicated Inbox context-service installations with the full existing isolation and activation profile, exact literal HOST-to-same-VIEW references, fixed interpreter commands and a 35-second stop deadline. Require native executable/dependency bytes and IPC resources to be exact members of the same complete installed profile.
|
|
16
|
+
|
|
17
|
+
- Expose selected staged boundary topology validation for installer source composition while retaining mandatory live authority socket verification for installed units.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Accept and validate the original workspace exclusion in the compact prepared worker release, keeping the native gate and authority producer closed schemas consistent.
|
|
22
|
+
|
|
23
|
+
- Verify protected ancestor directory identity and protection without treating unrelated sibling publication timestamps as selected-artifact mutation; regular-file metadata remains fully pinned.
|
|
24
|
+
|
|
25
|
+
- Return the protected task-context callback result only after unchanged artifact verification, preserving Bundle responses and raw context text while refusing post-callback mutation.
|
|
26
|
+
|
|
27
|
+
- Decode systemd InvocationID from its actual typed 16-byte D-Bus array, retaining canonical hexadecimal observations. Strictly verify the fixed manager pidfd-to-unit response and explicitly inherit only the held lifetime descriptor for that method.
|
|
28
|
+
|
|
29
|
+
- Preserve Ruby already-loaded builtin require no-ops only for the allowlisted startup features verified at first guard activation; later activation cannot grow that authority.
|
|
30
|
+
|
|
31
|
+
- Resolve guarded Ruby native-extension requests using the selected interpreter platform extension, including explicit `.so`/`.o` aliases, while authenticating only exact declared files.
|
|
32
|
+
|
|
33
|
+
- Reject duplicate JSON keys, invalid UTF-8, comments and excessive nesting in protected socket frames while preserving exact byte boundaries between successive frames.
|
|
34
|
+
|
|
35
|
+
- Authenticate the boot-selected private devpts backing and exact instance-local ptmx node before API exemption; shared host views refuse even when read-only. Verify the explicit unit projections and pinned contained ptmx link/node observations.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Launch the fixed worker through held interpreter and sealed wrapper descriptors, with bounded original artifact hashes and fixed OpenSSL digest initialization. Native execution remains an installed acceptance requirement.
|
|
40
|
+
|
|
41
|
+
- Resolve current network installation evidence through a fixed protected slot selection while retaining immutable static profile/producer intent and original historical references.
|
|
42
|
+
|
|
43
|
+
- Expose a scoped authenticated read-only duplicate of a retained artifact for exact inode handoff; verify before and after callbacks and close on every exit.
|
|
44
|
+
|
|
45
|
+
- Allow trusted installer code to select a bounded artifact count through `ProtectedArtifactSet.new(count_limit:)`; ordinary callers retain the 256-artifact default and both read interfaces enforce the same budget.
|
|
46
|
+
|
|
47
|
+
- Verify immutable per-boot original host baseline selections and the fixed readiness configuration/runtime load surface. Collect complete bounded server mount/IPC views through contained pinned descriptors and verify exact original backing plus the authenticated read-only authority socket projection. Trusted domain boot-proof production/refresh and installed effectiveness remain separate obligations.
|
|
48
|
+
|
|
49
|
+
## [0.2.0] - 2026-10-05
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Authenticate bounded root-produced network profile/report/trace artifacts with closed strict JSON schemas, exact context/check joins and held protected file descriptors. Missing or incomplete evidence refuses; effective network enforcement and immutable publication remain the domain installer’s responsibility.
|
|
54
|
+
|
|
55
|
+
- Verify fixed execution-unit artifact bytes and typed effective system-manager properties, including retained parent activation hierarchy, service enablement routes and exact native/readiness commands. Installation verification remains separate from live boundary and whole-scope proof.
|
|
56
|
+
|
|
57
|
+
- Add fixed system-systemd unit operations with bounded noninteractive jobs and descriptor-pinned cgroup-v2 population/membership observation. These primitives do not manufacture canonical scope proof or claim installed boundary acceptance.
|
|
58
|
+
|
|
59
|
+
- Ship the immutable native worker gate and protected Linux/socket primitives with exact process birth, pidfd exit, enforced Yama 2, empty capability sets and NoNewPrivs checks.
|
|
60
|
+
|
|
61
|
+
- Add read-only process ownership observation with PID birth identity, OS ancestry, unknown liveness and shared adapter contract coverage.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- Parse genuine typed nsfs mount roots while rejecting them as backing-storage projections, and query activation properties through the correct fixed slice and service interfaces.
|
|
66
|
+
|
|
9
67
|
## [0.1.1] - 2026-10-04
|
|
10
68
|
|
|
11
69
|
### Fixed
|
data/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Eleven operations, keyword-arg surfaces mirroring today's consumer calls:
|
|
|
18
18
|
|-----------|---------|
|
|
19
19
|
| Context | `runtime.context` → `{in_runtime:, session:, window:, pane:}` |
|
|
20
20
|
| Ensure window | `runtime.ensure_window(name:, root:, preset: nil)` — idempotent by normalized name; conflicting root/preset raises `WindowConflictError` |
|
|
21
|
+
| Process identity | `runtime.process_binding(pane:, caller_pid:)` → verified owner/native binding or nil (unknown); read-only, no authorization |
|
|
21
22
|
| Prepare pane | `runtime.prepare_pane(window:)` — splits when needed; target stays alive after a submitted command exits |
|
|
22
23
|
| Focus | `runtime.focus(window:)` |
|
|
23
24
|
| Send | `runtime.send(pane:, command: nil, items: [])` — ordered `{message: String}` / `{key: String}` entries |
|
|
@@ -118,3 +119,11 @@ adapter-authoring walkthrough.
|
|
|
118
119
|
Note: adapters intentionally define `send` (the intent operation),
|
|
119
120
|
which overrides `Object#send`. Use `__send__` for reflective dispatch
|
|
120
121
|
on adapter objects.
|
|
122
|
+
|
|
123
|
+
## Recoverable process ownership
|
|
124
|
+
|
|
125
|
+
`process_binding(pane:, caller_pid:)` identifies a caller's owner below the exact native retained shell. The returned string-keyed object binds `runtime`, `session`, `pane`, `shell_identity` and `process_identity`; each OS identity carries PID, UID, start time and host. Herdr additionally binds durable terminal ID, agent kind and immutable native agent session, and corroborates the owner against foreground process information. No command line arguments are stored.
|
|
126
|
+
|
|
127
|
+
An absent or non-native caller, retained shell without its owner, inaccessible OS facts, zombie or mismatched process returns nil. Native transport failures keep the standard typed error model. Consumers must compare the complete binding on each recovery observation. A process binding proves liveness only and grants no actor, service, effect or HITL authority. Unknown observation preserves resources; it does not prove all descendants stopped.
|
|
128
|
+
|
|
129
|
+
Process birth uses Linux boot ID with `/proc` start ticks or Darwin libproc microsecond birth. Whole-second `ps lstart` is never identity evidence. Unsupported/unreadable exact birth facilities return unknown. Capture brackets process metadata with birth reads, and native ownership revalidates the whole ancestor chain before returning it.
|
data/docs/usage.md
CHANGED
|
@@ -187,3 +187,126 @@ The reference implementation is `test/support/fake_runtime_adapter.rb`
|
|
|
187
187
|
in the ace-runtime gem; the in-gem suite
|
|
188
188
|
(`test/contract/adapter_contract_test.rb`) proves the battery against
|
|
189
189
|
it for both send profiles.
|
|
190
|
+
|
|
191
|
+
## Root-produced network installation evidence
|
|
192
|
+
|
|
193
|
+
The source-only `Ace::Runtime::Molecules::NetworkInstallationEvidence` consumer
|
|
194
|
+
checks the protected content selected by the installed scope owner:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
result = Ace::Runtime::Molecules::NetworkInstallationEvidence.verify!(
|
|
198
|
+
selection: boundary_manifest.fetch("network_installation"),
|
|
199
|
+
expected: {
|
|
200
|
+
"slot_id" => slot_id,
|
|
201
|
+
"namespace_path" => installed_namespace_path,
|
|
202
|
+
"boot_id" => current_boot_id,
|
|
203
|
+
"namespace_identity" => {"device" => pinned_device, "inode" => pinned_inode},
|
|
204
|
+
"installer_artifact_sha256" => installed_producer_sha256
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`selection` is exactly `{profile,policy_export,report,installer_artifact}`; each
|
|
210
|
+
reference is exactly `{path,sha256,bytes}`. All keys are strings, paths canonical
|
|
211
|
+
and absolute, SHA256 lowercase, and lengths positive integers. There is no
|
|
212
|
+
caller-uploaded evidence or caller-selected trust root. The namespace identity
|
|
213
|
+
above must come from the owner's actual held, authenticated nsfs network object;
|
|
214
|
+
the owner validates its type/current boot and keeps the namespace pinned through
|
|
215
|
+
admission. This verifier reads content artifacts and never opens/substitutes the
|
|
216
|
+
namespace, probes traffic or grants runtime networking privilege.
|
|
217
|
+
|
|
218
|
+
Successful output contains exactly `report_id`, `boot_id`, `slot_id`,
|
|
219
|
+
`namespace_path`, `namespace_identity`, `profile_sha256`, `policy_export_sha256`,
|
|
220
|
+
`report_sha256` and `installer_artifact_sha256`, deeply frozen. It authenticates
|
|
221
|
+
installation content, not actual enforcement from a summary boolean. Missing,
|
|
222
|
+
unreadable, malformed, conflicting, oversized or context-mismatched evidence
|
|
223
|
+
raises `Ace::Runtime::RuntimeUnavailableError`; the authority boundary exposes
|
|
224
|
+
that refusal as `evidence_unavailable` and retains public `invalid_input`
|
|
225
|
+
classification for malformed public selectors. No absent-evidence fallback.
|
|
226
|
+
|
|
227
|
+
The reader pins regular-file and directory descriptors for the complete graph,
|
|
228
|
+
checks exact lengths/digests and revalidates original objects/modes at completion.
|
|
229
|
+
It uses root ownership and no group/other write on supported Linux POSIX local
|
|
230
|
+
filesystems (`ext2`, `ext3`, `ext4`, `xfs`, `btrfs`, `tmpfs`): group mode bits
|
|
231
|
+
represent the access ACL mask, so named non-root ACL users/groups cannot obtain
|
|
232
|
+
write through it. Unknown filesystem models refuse. All ancestors are checked;
|
|
233
|
+
no symlink, untrusted writable ancestor or mutable worker artifact qualifies.
|
|
234
|
+
Content bounds are 1MiB per artifact, 16MiB aggregate and 256 distinct artifacts.
|
|
235
|
+
Trusted installer code verifying a larger explicitly selected source closure can
|
|
236
|
+
construct `ProtectedArtifactSet.new(count_limit: 4096)`; the value must be a
|
|
237
|
+
positive integer supplied by trusted code, never copied from an artifact or
|
|
238
|
+
request. Existing `file_limit:` and `total_limit:` budgets still apply. Mixed
|
|
239
|
+
`read!`/`read_path!` calls share the count; repeating a retained reference spends
|
|
240
|
+
no additional entry, and the first new artifact beyond the limit refuses.
|
|
241
|
+
Trusted source installers can use `with_readonly_handle!(reference)` inside an
|
|
242
|
+
active `ProtectedArtifactSet#with` callback to hand off the authenticated retained
|
|
243
|
+
regular file. It yields a read-only duplicate without reopening the selected
|
|
244
|
+
pathname, verifies the held set before and after the callback, and closes the
|
|
245
|
+
duplicate on success or error. No descriptor or maintenance authority survives
|
|
246
|
+
the callback. Existing reference/budget/root protection rules still apply.
|
|
247
|
+
|
|
248
|
+
Network evidence verifiers retain their default budgets. Held descriptors also
|
|
249
|
+
require sufficient process file-descriptor capacity in the installer environment.
|
|
250
|
+
The profile's 256-entry bound counts its four outer collections only; nested
|
|
251
|
+
ranges, ports and helper/configuration refs are bounded by the profile bytes and
|
|
252
|
+
separate artifact graph limits. Strict JSON parsing requires the declared
|
|
253
|
+
`json >= 2.20, < 3` dependency to reject duplicate keys and comments explicitly.
|
|
254
|
+
|
|
255
|
+
The trusted domain installer owns complete effective policy/topology comparison,
|
|
256
|
+
independent correlated flow checks, immutable digest-bearing evidence filenames
|
|
257
|
+
and publication that preserves historical artifacts. The generic reference
|
|
258
|
+
schema specifies no basename grammar: this consumer checks selected content and
|
|
259
|
+
protection, and cannot establish publication history. Raw policy/topology/setup
|
|
260
|
+
and correlated observations remain required; unsupported or incomplete domain
|
|
261
|
+
checks must never produce a passing report. Domain source/installed review proves
|
|
262
|
+
those obligations. For each loopback tool, the fixed domain procedure inspects
|
|
263
|
+
the actual selected executable against its profile `artifact_sha256` and retains
|
|
264
|
+
the observation in the exact tool-target trace. The schema provides no executable
|
|
265
|
+
path/ref: generic ACE authenticates the trace/raw refs and digest format; it does
|
|
266
|
+
not compare a tool executable digest with observation bytes or invent a ref.
|
|
267
|
+
Domain acceptance must exercise tool-executable mismatch refusal.
|
|
268
|
+
|
|
269
|
+
Root installation lifetime, private admission CAS, stage joins,
|
|
270
|
+
namespace containment and maintenance ordering belong to their respective
|
|
271
|
+
installed owners; this verifier does not implement them or supply native,
|
|
272
|
+
packet-filter or installed acceptance by itself.
|
|
273
|
+
|
|
274
|
+
### Original boot proof and fixed live readiness views
|
|
275
|
+
|
|
276
|
+
`ExecutionBootBaseline#select!(expected:)` authenticates the fixed protected
|
|
277
|
+
per-slot publication pointer at provisioning and returns its immutable proof
|
|
278
|
+
reference. `#verify!(selection:, expected:)` reads only that original reference,
|
|
279
|
+
joining whole-file bytes, slot, original boot, mapping digest, root PID1 birth
|
|
280
|
+
context and exact original network installer artifact. It never discovers an old
|
|
281
|
+
namespace or substitutes a current pointer. Proof JSON is closed and at most
|
|
282
|
+
16KiB. Trusted host capture, per-boot refresh and cold-start admission inhibition
|
|
283
|
+
remain domain gad.8/gad.b producer obligations.
|
|
284
|
+
|
|
285
|
+
`ServerResourceObservation` uses the same-UID native server's pinned root and
|
|
286
|
+
namespace descriptors without entering or changing a namespace. It reports the
|
|
287
|
+
complete mount table (at most256 rows), fixed API/tmp view mount IDs obtained from
|
|
288
|
+
contained O_PATH descriptors, server/hook IPC identities and the exact authority
|
|
289
|
+
socket identity. It also holds the original server’s `/proc/<pid>/ns/net` object,
|
|
290
|
+
requires the nsfs network-namespace type and rechecks its identity after collection.
|
|
291
|
+
The report includes that observed `network_namespace_identity`; it never substitutes
|
|
292
|
+
the hook’s namespace or a configured namespace identity. The private report shares
|
|
293
|
+
a65,536-byte ceiling. Mount path
|
|
294
|
+
prefixes, typed isolation flags and filesystem names alone do not prove live
|
|
295
|
+
isolation. `KernelViewTopology` verifies the fixed supported profile, read-only
|
|
296
|
+
API views, private IPC host difference and exact writable original backing;
|
|
297
|
+
unknown writable aliases and even declared writable API descendants refuse.
|
|
298
|
+
|
|
299
|
+
The fixed unit exposes only the selected read-only source-equals-view authority
|
|
300
|
+
socket and has `BindLogSockets=false`. Protected endpoint type/owner/incarnation,
|
|
301
|
+
contained view identity and authenticated peer remain separate required joins.
|
|
302
|
+
These observations supply admission evidence, not terminality or installed
|
|
303
|
+
effectiveness.
|
|
304
|
+
The supported devpts profile additionally requires an authenticated per-slot
|
|
305
|
+
`newinstance` backing, distinct from original host devpts. The trusted boot
|
|
306
|
+
baseline pins original host and selected instance identities plus the exact host
|
|
307
|
+
`pts/ptmx` link. The unit binds the selected root at `/dev/pts` and its read-only
|
|
308
|
+
ptmx node at `/dev/pts/ptmx`; the contained observer verifies the unchanged link,
|
|
309
|
+
character device and actual mount identity. `PrivateDevices` alone cannot prove
|
|
310
|
+
this isolation. Shared host RO/RW views and another slot's backing refuse.
|
|
311
|
+
gad.8 provisioning and gad.b refresh must deliver the selected source before
|
|
312
|
+
activation; controlled tests do not establish installed effectiveness.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "linux_mount_info"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Runtime
|
|
8
|
+
module Molecules
|
|
9
|
+
# A live descriptor pins the parent object, never the reusable pathname.
|
|
10
|
+
# Population is an observation only: canonical admission seal and verified
|
|
11
|
+
# resource boundaries remain the authority owner's responsibility.
|
|
12
|
+
class CgroupObservation
|
|
13
|
+
ROOT = "/sys/fs/cgroup"
|
|
14
|
+
IDENTITY_FIELDS = %w[path mount_id filesystem_type device inode].freeze
|
|
15
|
+
|
|
16
|
+
class KernelFiles
|
|
17
|
+
def open_directory(path)
|
|
18
|
+
File.open(path, File::RDONLY | File::NOFOLLOW)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def read(path, limit:)
|
|
22
|
+
File.open(path, File::RDONLY | File::NOFOLLOW) do |file|
|
|
23
|
+
bytes = file.read(limit + 1)
|
|
24
|
+
raise RuntimeUnavailableError, "kernel scope observation is oversized" if bytes.bytesize > limit
|
|
25
|
+
bytes
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def mount_identity(handle)
|
|
30
|
+
info = read("/proc/self/fdinfo/#{handle.fileno}", limit: 4096)
|
|
31
|
+
matches = info.lines.filter_map { |line| /\Amnt_id:\s+([0-9]+)\s*\z/.match(line)&.[](1) }
|
|
32
|
+
raise RuntimeUnavailableError, "scope mount identity is unavailable" unless matches.size == 1
|
|
33
|
+
mount_id = Integer(matches.first, 10)
|
|
34
|
+
mount = LinuxMountInfo.new(read("/proc/self/mountinfo", limit: LinuxMountInfo::LIMIT)).by_id(mount_id)
|
|
35
|
+
mount.slice("mount_id", "filesystem_type", "root", "mountpoint")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(files: KernelFiles.new)
|
|
40
|
+
@files = files
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pin(path, expected: nil)
|
|
44
|
+
validate_path!(path)
|
|
45
|
+
handle = @files.open_directory(path)
|
|
46
|
+
identity = identity_for(handle, path)
|
|
47
|
+
unless expected.nil? || valid_identity?(expected) && identity == expected
|
|
48
|
+
raise RuntimeUnavailableError, "retained scope object was replaced"
|
|
49
|
+
end
|
|
50
|
+
verify_path!(handle, identity)
|
|
51
|
+
{handle: handle, identity: identity.freeze}
|
|
52
|
+
rescue SystemCallError, IOError
|
|
53
|
+
handle&.close
|
|
54
|
+
raise RuntimeUnavailableError, "exact scope object cannot be pinned"
|
|
55
|
+
rescue Exception
|
|
56
|
+
handle&.close
|
|
57
|
+
raise
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def observe(pinned)
|
|
61
|
+
handle, identity = pinned.values_at(:handle, :identity)
|
|
62
|
+
unless handle && !handle.closed? && valid_identity?(identity) && identity_for(handle, identity.fetch("path")) == identity
|
|
63
|
+
raise RuntimeUnavailableError, "pinned scope identity changed"
|
|
64
|
+
end
|
|
65
|
+
verify_path!(handle, identity)
|
|
66
|
+
bytes = @files.read("/proc/self/fd/#{handle.fileno}/cgroup.events", limit: 4096)
|
|
67
|
+
fields = parse_events(bytes)
|
|
68
|
+
verify_path!(handle, identity)
|
|
69
|
+
{"cgroup_identity" => identity.dup, "populated" => fields.fetch("populated")}
|
|
70
|
+
rescue SystemCallError, IOError, KeyError
|
|
71
|
+
raise RuntimeUnavailableError, "retained scope cannot be observed"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Membership checks use the caller's independently pinned birth policy.
|
|
75
|
+
# A namespace-relative cgroup value must agree with the host parent;
|
|
76
|
+
# unresolved membership refuses rather than following another mount.
|
|
77
|
+
def member!(identity, pinned:, kernel:)
|
|
78
|
+
kernel.live!(identity)
|
|
79
|
+
observe(pinned)
|
|
80
|
+
bytes = @files.read("/proc/#{identity.fetch('pid')}/cgroup", limit: 4096)
|
|
81
|
+
paths = bytes.lines.filter_map { |line| /\A0::(\/[^\n]*)\n?\z/.match(line)&.[](1) }
|
|
82
|
+
parent = pinned.fetch(:identity).fetch("path").delete_prefix(ROOT)
|
|
83
|
+
unless paths.size == 1 && (paths.first == parent || paths.first.start_with?(parent + "/"))
|
|
84
|
+
raise RuntimeUnavailableError, "process is outside the exact execution scope"
|
|
85
|
+
end
|
|
86
|
+
kernel.live!(identity)
|
|
87
|
+
observe(pinned)
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_events(bytes)
|
|
92
|
+
unless bytes.is_a?(String) && bytes.bytesize.between?(1, 4096)
|
|
93
|
+
raise RuntimeUnavailableError, "cgroup population is unavailable"
|
|
94
|
+
end
|
|
95
|
+
fields = {}
|
|
96
|
+
bytes.each_line do |line|
|
|
97
|
+
match = /\A([a-z_]+) ([0-9]+)\n?\z/.match(line)
|
|
98
|
+
unless match && !fields.key?(match[1])
|
|
99
|
+
raise RuntimeUnavailableError, "cgroup events are malformed"
|
|
100
|
+
end
|
|
101
|
+
fields[match[1]] = Integer(match[2], 10)
|
|
102
|
+
end
|
|
103
|
+
unless [0, 1].include?(fields["populated"])
|
|
104
|
+
raise RuntimeUnavailableError, "cgroup population is not a positive kernel observation"
|
|
105
|
+
end
|
|
106
|
+
fields
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def valid_identity?(value)
|
|
112
|
+
value.is_a?(Hash) && value.keys.sort == IDENTITY_FIELDS.sort &&
|
|
113
|
+
value["path"].is_a?(String) && value["filesystem_type"] == "cgroup2" && %w[mount_id device inode].all? do |key|
|
|
114
|
+
value[key].is_a?(Integer) && value[key] >= 0
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_path!(path)
|
|
119
|
+
unless path.is_a?(String) && path.bytesize <= 4096 && !path.include?("\0") &&
|
|
120
|
+
path.start_with?(ROOT + "/") && File.expand_path(path) == path && path != ROOT
|
|
121
|
+
raise RuntimeUnavailableError, "scope must be an exact parent below the unified cgroup root"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def identity_for(handle, path)
|
|
126
|
+
stat = handle.stat
|
|
127
|
+
mount = @files.mount_identity(handle)
|
|
128
|
+
unless stat.directory? && mount["filesystem_type"] == "cgroup2" &&
|
|
129
|
+
mount["mountpoint"] == ROOT && mount["root"] == "/"
|
|
130
|
+
raise RuntimeUnavailableError, "scope requires the host unified cgroup-v2 mount"
|
|
131
|
+
end
|
|
132
|
+
{"path" => path, "mount_id" => mount.fetch("mount_id"), "filesystem_type" => "cgroup2",
|
|
133
|
+
"device" => stat.dev, "inode" => stat.ino}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def verify_path!(handle, identity)
|
|
137
|
+
current = @files.open_directory(identity.fetch("path"))
|
|
138
|
+
unless identity_for(current, identity.fetch("path")) == identity && handle.stat.ino == current.stat.ino
|
|
139
|
+
raise RuntimeUnavailableError, "scope pathname no longer names its pinned object"
|
|
140
|
+
end
|
|
141
|
+
true
|
|
142
|
+
ensure
|
|
143
|
+
current&.close
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Runtime
|
|
5
|
+
module Molecules
|
|
6
|
+
# Private specialization of the SAME immutable/effective installation owner.
|
|
7
|
+
class ExecutionUnitInstallation
|
|
8
|
+
CODEX_TOKEN = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/.freeze
|
|
9
|
+
CODEX_INTENT_FIELDS = %w[codex credentials cwd dependencies environment home inbox_context_id native_mapping_id project_id schema socket_gid socket_path thread_configuration unit_name].freeze
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def initialize_codex_runtime(service, reference, mapping, mapping_id, authority, files)
|
|
14
|
+
unless service.is_a?(Hash) && service.keys.sort == %w[boundary_manifest execution_scope unit_manifest] &&
|
|
15
|
+
mapping.is_a?(Hash) && mapping_id.is_a?(String) && SystemdScopeManager::UNIT.match?(mapping_id)
|
|
16
|
+
raise RuntimeUnavailableError, "Codex service selection differs"
|
|
17
|
+
end
|
|
18
|
+
[reference, *service.values_at("unit_manifest", "boundary_manifest")].each { |ref| codex_reference!(ref, 65_536) }
|
|
19
|
+
bytes = files.read(reference.fetch("path"), limit: reference.fetch("bytes"))
|
|
20
|
+
unless bytes.bytesize == reference.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == reference.fetch("sha256")
|
|
21
|
+
raise RuntimeUnavailableError, "Codex intent bytes differ"
|
|
22
|
+
end
|
|
23
|
+
intent = JSON.parse(bytes, create_additions: false, max_nesting: 32, allow_duplicate_key: false, allow_comments: false)
|
|
24
|
+
unless intent.is_a?(Hash) && intent.keys.sort == CODEX_INTENT_FIELDS.sort &&
|
|
25
|
+
intent["schema"] == "lab.codex-runtime-intent/v1" &&
|
|
26
|
+
intent.values_at("project_id", "native_mapping_id", "cwd") == [mapping.fetch("project_id"), mapping_id, mapping.fetch("worker_cwd")] &&
|
|
27
|
+
intent["unit_name"].is_a?(String) && SystemdScopeManager::UNIT.match?(intent["unit_name"]) && intent["unit_name"].end_with?(".service") &&
|
|
28
|
+
intent["credentials"] == {"uid" => mapping.fetch("worker_uid"), "gid" => mapping.fetch("worker_gid"), "groups" => mapping.fetch("worker_groups")}
|
|
29
|
+
raise RuntimeUnavailableError, "Codex intent original mapping differs"
|
|
30
|
+
end
|
|
31
|
+
credentials = intent.fetch("credentials")
|
|
32
|
+
unless credentials.values_at("uid", "gid").all? { |id| id.is_a?(Integer) && id.positive? && id <= (1 << 32) - 2 } &&
|
|
33
|
+
credentials["groups"].is_a?(Array) && credentials["groups"].size <= 64 &&
|
|
34
|
+
credentials["groups"] == credentials["groups"].sort.uniq &&
|
|
35
|
+
credentials["groups"].all? { |id| id.is_a?(Integer) && id.between?(0, (1 << 32) - 2) }
|
|
36
|
+
raise RuntimeUnavailableError, "Codex original native credentials differ"
|
|
37
|
+
end
|
|
38
|
+
original = mapping.fetch("execution_scope")
|
|
39
|
+
scope = service.fetch("execution_scope")
|
|
40
|
+
changes = %w[service_unit unit_manifest_sha256]
|
|
41
|
+
unless scope.is_a?(Hash) && scope.keys.sort == %w[backend boundary_manifest_sha256 network_namespace_path root_directory runtime_directory service_unit slice_unit slot_id unit_manifest_sha256] &&
|
|
42
|
+
scope["backend"] == "linux_systemd_cgroup_v2" && scope.keys.sort == original.keys.sort &&
|
|
43
|
+
scope.reject { |key, _| changes.include?(key) } == original.reject { |key, _| changes.include?(key) } &&
|
|
44
|
+
scope["service_unit"] == intent.fetch("unit_name") && scope["service_unit"] != original.fetch("service_unit") &&
|
|
45
|
+
scope["unit_manifest_sha256"] == service.fetch("unit_manifest").fetch("sha256") &&
|
|
46
|
+
scope["boundary_manifest_sha256"] == service.fetch("boundary_manifest").fetch("sha256")
|
|
47
|
+
raise RuntimeUnavailableError, "Codex service expands original containment"
|
|
48
|
+
end
|
|
49
|
+
codex_reference!(intent.fetch("codex"), 268_435_456)
|
|
50
|
+
dependencies = intent.fetch("dependencies")
|
|
51
|
+
unless dependencies.is_a?(Array) && dependencies.size <= 506 && ([intent.fetch("codex")] + dependencies).map { |ref| ref.fetch("path") }.uniq.size == dependencies.size + 1
|
|
52
|
+
raise RuntimeUnavailableError, "Codex selected dependency inventory differs"
|
|
53
|
+
end
|
|
54
|
+
dependencies.each { |ref| codex_reference!(ref, 268_435_456) }
|
|
55
|
+
unless ([intent.fetch("codex")] + dependencies).sum { |ref| ref.fetch("bytes") } <= 268_435_456 &&
|
|
56
|
+
path?(intent["cwd"]) && path?(intent["home"]) && path?(intent["socket_path"]) &&
|
|
57
|
+
intent["socket_path"].encoding == Encoding::UTF_8 && intent["socket_path"].valid_encoding? && intent["socket_path"].bytesize <= 107 &&
|
|
58
|
+
intent["socket_gid"].is_a?(Integer) && intent["socket_gid"].between?(1, (1 << 32) - 2) &&
|
|
59
|
+
intent["inbox_context_id"].is_a?(String) && CODEX_TOKEN.match?(intent["inbox_context_id"]) &&
|
|
60
|
+
intent["thread_configuration"].is_a?(Hash) && intent["thread_configuration"].keys.sort == %w[approval_policy model sandbox] &&
|
|
61
|
+
intent["thread_configuration"].values_at("approval_policy", "sandbox") == ["never", "danger-full-access"] &&
|
|
62
|
+
intent["thread_configuration"]["model"].is_a?(String) && CODEX_TOKEN.match?(intent["thread_configuration"]["model"])
|
|
63
|
+
raise RuntimeUnavailableError, "Codex runtime input bounds differ"
|
|
64
|
+
end
|
|
65
|
+
environment = intent.fetch("environment")
|
|
66
|
+
unless environment.is_a?(Hash) && environment.size <= 32 && environment.all? { |key, value|
|
|
67
|
+
NATIVE_ENVIRONMENT.include?(key) && value.is_a?(String) && value.encoding == Encoding::UTF_8 &&
|
|
68
|
+
value.valid_encoding? && value.bytesize <= 4096 && !value.include?("\0") } &&
|
|
69
|
+
(!environment.key?("HOME") || environment["HOME"] == intent.fetch("home"))
|
|
70
|
+
raise RuntimeUnavailableError, "Codex runtime environment differs"
|
|
71
|
+
end
|
|
72
|
+
@codex_service, @codex_intent = immutable_selection(service), immutable_selection(intent)
|
|
73
|
+
@codex_intent_reference, @codex_authority = immutable_selection(reference), immutable_selection(authority)
|
|
74
|
+
@scope, @files = immutable_selection(scope), files
|
|
75
|
+
@worker_uid, @worker_gid, @codex_groups = credentials.values_at("uid", "gid", "groups")
|
|
76
|
+
@codex_groups = immutable_selection(@codex_groups)
|
|
77
|
+
rescue KeyError, TypeError, NoMethodError, ArgumentError, JSON::ParserError
|
|
78
|
+
raise RuntimeUnavailableError, "Codex installation selection is incomplete"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def codex_reference!(ref, limit)
|
|
82
|
+
unless ref.is_a?(Hash) && ref.keys.sort == %w[bytes path sha256] && path?(ref["path"]) &&
|
|
83
|
+
ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, limit) &&
|
|
84
|
+
ref["sha256"].is_a?(String) && ref["sha256"].match?(/\A[0-9a-f]{64}\z/)
|
|
85
|
+
raise RuntimeUnavailableError, "Codex immutable reference differs"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def codex_reference_bytes!(ref)
|
|
90
|
+
bytes = @files.read(ref.fetch("path"), limit: ref.fetch("bytes"))
|
|
91
|
+
unless bytes.bytesize == ref.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == ref.fetch("sha256")
|
|
92
|
+
raise RuntimeUnavailableError, "Codex selected artifact bytes differ"
|
|
93
|
+
end
|
|
94
|
+
bytes
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def verify_codex_artifacts!(roles)
|
|
98
|
+
{"native_executable" => @codex_intent.fetch("codex"), "native_configuration" => @codex_intent_reference,
|
|
99
|
+
"boundary_manifest" => @codex_service.fetch("boundary_manifest")}.each do |role, ref|
|
|
100
|
+
artifact = roles.fetch(role).first
|
|
101
|
+
unless artifact.values_at("host_path", "view_path", "sha256") == ref.values_at("path", "path", "sha256") &&
|
|
102
|
+
codex_reference_bytes!(ref).bytesize == ref.fetch("bytes")
|
|
103
|
+
raise RuntimeUnavailableError, "Codex artifact differs from original selection"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
unless roles.fetch("runtime_dependency", []).size == @codex_intent.fetch("dependencies").size
|
|
107
|
+
raise RuntimeUnavailableError, "Codex installed dependency inventory differs"
|
|
108
|
+
end
|
|
109
|
+
@codex_intent.fetch("dependencies").each do |ref|
|
|
110
|
+
selected = roles.fetch("runtime_dependency", []).select { |artifact| artifact.values_at("host_path", "view_path", "sha256") == ref.values_at("path", "path", "sha256") }
|
|
111
|
+
unless selected.one? && codex_reference_bytes!(ref).bytesize == ref.fetch("bytes")
|
|
112
|
+
raise RuntimeUnavailableError, "Codex selected dependency is not installed"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
roles
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def verify_codex_protocol!(service, artifacts)
|
|
119
|
+
executable = @codex_intent.fetch("codex").fetch("path")
|
|
120
|
+
verify_command!(service.fetch("ExecStartEx"), executable)
|
|
121
|
+
expected = @codex_intent.fetch("environment").merge("HOME" => @codex_intent.fetch("home")).map { |key, value| "#{key}=#{value}" }.sort
|
|
122
|
+
unless service.fetch("ExecStartEx").first[1] == [executable, "app-server", "--listen", "unix://" + @codex_intent.fetch("socket_path")] &&
|
|
123
|
+
service.fetch("ExecStartPostEx") == [] && service.fetch("Environment").sort == expected
|
|
124
|
+
raise RuntimeUnavailableError, "Codex unit commands differ from fixed startup protocol"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "protected_artifact_set"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Runtime
|
|
7
|
+
module Molecules
|
|
8
|
+
# Content authentication over the existing trusted host installer proof.
|
|
9
|
+
# Actual original-host capture and boot refresh belong to gad.8/gad.b.
|
|
10
|
+
class ExecutionBootBaseline
|
|
11
|
+
LIMIT = 16_384
|
|
12
|
+
ID = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/
|
|
13
|
+
SHA = /\A[0-9a-f]{64}\z/
|
|
14
|
+
BOOT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
|
|
15
|
+
EXPECTED = %w[boot_id deployment_digest installer_artifact slot_id].freeze
|
|
16
|
+
FIELDS = %w[boot_id deployment_digest host_devpts_identity host_ipc_namespace_identity host_ptmx_link original_host_context producer_artifact schema selected_devpts slot_id].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(artifacts: ProtectedArtifactSet.new)
|
|
19
|
+
@artifacts = artifacts
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Fresh provisioning only. Never called for historical authentication.
|
|
23
|
+
def select!(expected:)
|
|
24
|
+
expected!(expected)
|
|
25
|
+
@artifacts.with do
|
|
26
|
+
path = "/etc/ace/execution-slots/#{expected.fetch('slot_id')}/boot-baseline-selection.json"
|
|
27
|
+
bytes, = @artifacts.read_path!(path, limit: LIMIT)
|
|
28
|
+
pointer = json!(bytes)
|
|
29
|
+
object!(pointer, %w[baseline schema slot_id])
|
|
30
|
+
refuse! unless pointer["schema"] == "ace.execution-boot-selection/v1" && pointer["slot_id"] == expected.fetch("slot_id")
|
|
31
|
+
selected = pointer.fetch("baseline")
|
|
32
|
+
baseline = authenticate!(selected, expected)
|
|
33
|
+
@artifacts.verify_unchanged!
|
|
34
|
+
immutable({"selection" => selected, "baseline" => baseline})
|
|
35
|
+
end
|
|
36
|
+
rescue KeyError, TypeError, NoMethodError, ArgumentError, IOError, SystemCallError, JSON::ParserError
|
|
37
|
+
refuse!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Pure original-ref reader: expected boot comes from canonical lineage,
|
|
41
|
+
# not current discovery. Runtime owners separately check current boot.
|
|
42
|
+
def verify!(selection:, expected:)
|
|
43
|
+
expected!(expected)
|
|
44
|
+
@artifacts.with do
|
|
45
|
+
value = authenticate!(selection, expected)
|
|
46
|
+
@artifacts.verify_unchanged!
|
|
47
|
+
immutable(value)
|
|
48
|
+
end
|
|
49
|
+
rescue KeyError, TypeError, NoMethodError, ArgumentError, IOError, SystemCallError, JSON::ParserError
|
|
50
|
+
refuse!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def authenticate!(selected, expected)
|
|
56
|
+
reference!(selected, limit: LIMIT)
|
|
57
|
+
reference!(expected.fetch("installer_artifact"))
|
|
58
|
+
value = json!(@artifacts.read!(selected))
|
|
59
|
+
object!(value, FIELDS)
|
|
60
|
+
reference!(value.fetch("producer_artifact"))
|
|
61
|
+
unless value["schema"] == "ace.execution-boot-baseline/v1" &&
|
|
62
|
+
value.values_at("boot_id", "deployment_digest", "slot_id") == expected.values_at("boot_id", "deployment_digest", "slot_id") &&
|
|
63
|
+
value["producer_artifact"] == expected.fetch("installer_artifact")
|
|
64
|
+
refuse!
|
|
65
|
+
end
|
|
66
|
+
@artifacts.read!(expected.fetch("installer_artifact"))
|
|
67
|
+
identity = value.fetch("host_ipc_namespace_identity")
|
|
68
|
+
object!(identity, %w[device inode])
|
|
69
|
+
refuse! unless identity.values.all? { |number| number.is_a?(Integer) && number.positive? }
|
|
70
|
+
host = value.fetch("host_devpts_identity")
|
|
71
|
+
selected = value.fetch("selected_devpts")
|
|
72
|
+
object!(host, %w[device inode major_minor])
|
|
73
|
+
object!(selected, %w[device inode major_minor path ptmx_inode])
|
|
74
|
+
[host, selected].each do |instance|
|
|
75
|
+
refuse! unless %w[device inode].all? { |key| instance[key].is_a?(Integer) && instance[key].positive? } &&
|
|
76
|
+
instance["major_minor"].is_a?(String) && instance["major_minor"].match?(/\A(?:0|[1-9][0-9]*):(?:0|[1-9][0-9]*)\z/)
|
|
77
|
+
end
|
|
78
|
+
refuse! unless value["host_ptmx_link"] == "pts/ptmx" &&
|
|
79
|
+
selected["path"] == "/run/ace/execution-slots/#{expected.fetch('slot_id')}/devpts" &&
|
|
80
|
+
selected["device"] != host["device"] && selected["major_minor"] != host["major_minor"] &&
|
|
81
|
+
selected["ptmx_inode"].is_a?(Integer) && selected["ptmx_inode"].positive?
|
|
82
|
+
context = value.fetch("original_host_context")
|
|
83
|
+
object!(context, %w[gid pid started_at uid])
|
|
84
|
+
unless context.values_at("pid", "uid", "gid").all? { |number| number.is_a?(Integer) } &&
|
|
85
|
+
context.values_at("pid", "uid", "gid") == [1, 0, 0] && context["started_at"].is_a?(String) &&
|
|
86
|
+
context["started_at"].match?(/\Alinux:#{Regexp.escape(expected.fetch('boot_id'))}:(?:0|[1-9][0-9]*)\z/) &&
|
|
87
|
+
context["started_at"].bytesize <= 128
|
|
88
|
+
refuse!
|
|
89
|
+
end
|
|
90
|
+
value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def expected!(value)
|
|
94
|
+
object!(value, EXPECTED)
|
|
95
|
+
refuse! unless value["slot_id"].is_a?(String) && ID.match?(value["slot_id"]) &&
|
|
96
|
+
value["boot_id"].is_a?(String) && BOOT.match?(value["boot_id"]) &&
|
|
97
|
+
value["deployment_digest"].is_a?(String) && SHA.match?(value["deployment_digest"])
|
|
98
|
+
reference!(value.fetch("installer_artifact"))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def reference!(value, limit: ProtectedArtifactSet::LIMIT)
|
|
102
|
+
object!(value, %w[bytes path sha256])
|
|
103
|
+
refuse! unless value["path"].is_a?(String) && value["path"].bytesize.between?(1, 4096) &&
|
|
104
|
+
value["path"].start_with?("/") && !value["path"].include?("\0") && File.expand_path(value["path"]) == value["path"] &&
|
|
105
|
+
value["bytes"].is_a?(Integer) && value["bytes"].between?(1, limit) &&
|
|
106
|
+
value["sha256"].is_a?(String) && SHA.match?(value["sha256"])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def json!(bytes)
|
|
110
|
+
bytes = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
111
|
+
refuse! unless bytes.bytesize.between?(1, LIMIT) && bytes.valid_encoding?
|
|
112
|
+
JSON.parse(bytes, create_additions: false, max_nesting: 8, allow_duplicate_key: false, allow_comments: false)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def object!(value, keys)
|
|
116
|
+
refuse! unless value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def immutable(value)
|
|
120
|
+
case value
|
|
121
|
+
when Hash then value.to_h { |key, item| [key.dup.freeze, immutable(item)] }.freeze
|
|
122
|
+
when Array then value.map { |item| immutable(item) }.freeze
|
|
123
|
+
when String then value.dup.freeze
|
|
124
|
+
else value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def refuse!
|
|
129
|
+
raise RuntimeUnavailableError, "authenticated original host boot baseline unavailable"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|