Author SHA1 Message Date
Codex 11b73e6468 feat(deploy): add scoped Mission Core map gateway access 2026-09-10 20:48:04 +03:00
Codex 6e2baf16fc feat(deploy): add Tasker attachment formats artifact 2026-08-29 16:09:52 +03:00
Codex dcf5304345 feat(deploy): register Device Core telemetry releases 2026-08-23 11:58:58 +03:00
Codex 680ba0285e fix(deploy): accept exact replay checkpoints 2026-08-22 23:58:17 +03:00
Codex c3bd8023d6 fix(deploy): model replay 011 predecessor 2026-08-22 23:11:10 +03:00
Codex 853e423643 fix(deploy): use canonical audit state paths 2026-08-22 22:37:37 +03:00
Codex 7b0e6d0bdf chore(deploy): register migration invariant audit 2026-08-22 22:11:35 +03:00
Codex e642113124 fix(deploy): recover control core migration replay 2026-08-22 21:12:27 +03:00
Codex bd715e9d71 chore(deploy): register read-only Core incident audit 2026-08-22 20:44:15 +03:00
Codex 8d501224b4 fix(deploy): restore exact preapply Core image 2026-08-22 19:49:36 +03:00
Codex c5c4e69acb feat(deploy): register VPS telemetry releases 2026-08-22 17:29:46 +03:00
Codex 0438fbd80b feat(deploy): register canonical manager v8 2026-08-22 15:33:06 +03:00
Codex e2ea7e177d feat(ontology): add device asset infrastructure domains 2026-08-22 14:15:57 +03:00
Codex 209f4f439e merge: canonicalize Device Plane release runner 2026-08-22 13:15:09 +03:00
Codex a6771b2fbf deploy: register Manager v7 VPS health projection 2026-08-22 13:08:38 +03:00
Codex 502acd3771 feat(deploy): add canonical Gitea recovery flows 2026-08-22 11:14:33 +03:00
Codex 87a1e64807 deploy: register Manager v6 favicon release 2026-08-22 10:50:03 +03:00
Codex 952290a49d deploy: register Manager v5 landing parity 2026-08-22 10:23:07 +03:00
Codex 6123a06527 deploy: register Manager v4 persistent data 2026-08-22 09:47:57 +03:00
Codex 12901e0e19 fix(deploy): accept Manager v3 private egress 2026-08-22 09:09:32 +03:00
Codex a6fb60d38f deploy: pin manager v3 to Core recovery 2026-08-22 02:10:42 +03:00
Codex 94e437e842 fix(deploy): accept selected Core restart states 2026-08-22 01:43:11 +03:00
Codex c1c9818e30 fix(deploy): allow unhealthy selected Core recovery 2026-08-22 01:27:00 +03:00
66 changed files with 23122 additions and 96 deletions
@@ -0,0 +1 @@
{"schemaVersion":"nodedc.mission-core-map-access.v1","state":"enabled"}
+338
View File
@@ -81,9 +81,347 @@ Supported components in this source:
- `n8n-private-extension`
- `module-foundry`
- `device-plane`
- `gitea`
- `proxy-contur`
- `dc-amd-proxy`
## Fresh Gitea installation
`gitea` is a one-time, fresh-install-only component. Its artifact contains
only the reviewed Compose file and strict deployment descriptor; it contains
no database, repository, user, token, key, hook, runtime data or secret. Build
the deterministic artifact locally with:
```bash
node infra/deploy-runner/build-gitea-fresh-install-artifact.mjs \
gitea-fresh-install-YYYYMMDD-NNN
```
The runtime is fixed to Gitea `1.27.1-rootless` by exact digest and
`linux/amd64`. The runner never pulls it: the exact image must first be loaded
through a separately reviewed trusted image-acquisition step. Compose is
invoked with `--pull never --no-deps`, uses `network_mode: none`, publishes no
ports, and serves only the host-mounted Unix socket
`/volume1/docker/nodedc-gitea/socket/gitea.sock`. It exposes no SSH port, uses
fresh SQLite, enables no LFS, and mounts two runner-generated secrets by URI.
Registration and password Basic auth are disabled, while MFA is enforced;
hooks, webhooks, migrations, packages, Actions, OAuth2/OpenID, federation and
update checks are disabled. HTTP Git with PAT remains the intended trusted
client path; PAT clone/push acceptance belongs to the later reviewed
user/repository restoration transition, not this empty installation. LFS may
only be enabled by that later transition after its secret and restart behavior
has been accepted against the pinned runtime.
Plan and apply fail closed unless all live prerequisites are already true:
- `/volume1/docker/nodedc-gitea`, the `nodedc-gitea` project, and any stale
`nodedc-gitea_internal` network are absent; TCP/4022 is closed and Docker
publishes neither TCP/3000 nor TCP/4022;
- Docker Server is exactly 24.0.2, Compose is at least 2.20.1, and the exact
pinned image ID and RepoDigest are local for linux/amd64 with image user
`1000:1000`;
- the separate root-owned Nginx bridge
`/usr/local/etc/nginx/conf.d/http.nodedc-gitea-uds.conf` matches its exact
reviewed bytes, owner/mode and SHA-256; Nginx 1.23.1 configuration validates,
and only its root/uid-1023 processes own `127.0.0.1:3000`. The bridge proxies
to the Unix socket, preserves Host/X-Real-IP/XFF/XFP, rate-limits login and
bounds concurrent requests without limiting Git request bodies;
- DSM reverse-proxy UUID `5bc46027-0307-4261-af7e-4f94a3c508c9` persistently
and in generated nginx config routes `git.dcserve.ru:443` to
`127.0.0.1:3000`; the generated vhost must also retain its exact host guard
and canonical Host/X-Real-IP/XFF/XFP forwarding directives, so the UDS
bridge never trusts a client-supplied `X-Real-IP`;
- the emergency broad INPUT DROP for TCP/3000 has been removed, while the
legacy `172.22.0.222:3000` OUTPUT DROP and TCP/4022 INPUT DROP remain;
- any legacy container named `gitea` is stopped with restart policy `no`.
Changing DSM reverse proxy/firewall rules, acquiring evidence from the old
root, restoring trusted users/repositories, and loading the pinned image are
separate reviewed transitions. This component never reads or mounts
`/volume1/docker/gitea` and never starts the legacy container. Failed apply
stops/removes only the `nodedc-gitea` candidate, proves that it is absent, and
quarantines the complete newly created root (including data/config/socket/secrets) for
evidence, and restores the absent source/root state without deleting runtime
evidence. If candidate absence cannot be proven, rollback preserves the root in
place and records reconciliation-required instead of disconnecting live bind
mounts.
Run the policy suite before publishing:
```bash
PYTHONDONTWRITEBYTECODE=1 \
python3 infra/deploy-runner/test_gitea_fresh_install.py -v
```
## Gitea incident salvage (clean-state recovery)
The additive `gitea-incident-salvage` slice is the reviewed recovery design for
incident `gitea-20260814`. It does not upgrade or mount the compromised state.
The candidate starts with a new SQLite database, new config, new runtime
secrets, new email addresses and new password hashes. The exact v2 decision
bundle is embedded as inert, digest-bound data:
- 2 new active identities (`dctouch`, admin; `SILVER`, non-admin), each with a
new root-contained one-time password and mandatory password change;
- 8 new locked identities with random discarded passwords, inactive,
restricted and prohibited from login;
- 45 exact repositories: 32 private `dctouch` repositories and 13 public
`SILVER` repositories. The other 962 legacy users and 2013 legacy repository
rows are not imported into the clean database.
The successor v3 descriptor embeds the owner-confirmed, canonical
`confirmed-disposition-v1.json` and the additive
`confirmed-closure-disposition-v1.json`. Both are bound to the exact snapshot,
database, identity decision, reference manifest, unsupported-state report,
schema catalog and semantic-topics hashes. The original disposition's 105
reference decisions are explicit rows, not namespace wildcards: 85 normal
heads, 4 tags and 4 wiki heads are eligible for later live reconstruction; the
exact 5 pull refs and 7 remote refs are sealed archive-only evidence and can
never be auto-promoted. The closure disposition authorizes only bounded
plan-time inventory and records the target policy for access, collaboration,
issue/PR metadata, attachments, releases, labels, projects, units, Packages
and Actions. Observed hashes do not become activation authority, and the new
closure-report digest is deliberately unpinned until separate review.
Build the deterministic review artifact locally with:
```bash
node infra/deploy-runner/build-gitea-incident-salvage-artifact.mjs \
gitea-incident-salvage-subrelation-closure-20260814-006
```
The original v1 descriptor and `...salvage-20260814-001.tgz` remain immutable.
The exact `...salvage-disposition-20260814-002.tgz` also remains immutable and
must not be retried: its plan failed safely because it assumed every nonempty
`repository.topics` TEXT value was a JSON array. A bounded immutable-database
classification proved all 45 kept rows are instead exact four-byte canonical
JSON `null`, Gitea's semantic empty representation. The `...topics-...-003`
artifact is the forward fix with a new patch id and digest.
That exact `...topics-...-003` artifact is now also an immutable predecessor.
The local pre-audit `...closure-...-004` draft must not be staged: review found
orphan-join and nullable-Actions gaps. The local
`...closure-reviewed-...-005` artifact is also stale and must not be staged:
its primary closure was fail-closed, but it retained a broad polymorphic-
subrelation blocker. The additive `...subrelation-closure-...-006` successor
closes the concrete comment/history/merger relations in the pinned snapshot
schema and is the only artifact produced by the current builder.
The successor builder refuses to overwrite an existing artifact basename and
publishes from an exclusively created same-directory temporary file using an
atomic no-clobber hard link. A failed build removes only its private temporary
file. The disposition is sorted, compact ASCII JSON with one terminal LF.
The runtime pin is
`docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49a…3abd2c`, with exact
linux/amd64 OCI image ID `sha256:272085a8…bc5c4c6`. It runs as `1000:1000`,
uses the direct Gitea binary, `network_mode: none`, no Docker ports, no SSH and
no LFS. Candidate restart policy is `no`. The existing reviewed Nginx bridge
cannot reach the candidate while the socket parent is mode `0700`; publication
is an explicit post-acceptance change to `0750`, followed by public TLS and
smart-HTTP checks. Only after those checks may restart policy become
`unless-stopped`. The transition never changes or enforces two-factor
authentication.
The snapshot boundary is exact: UUID
`f5a3fe3a-93ea-bb4d-847f-6221a6bcbc9f`, immutable SQLite SHA-256
`8db9e74a…1a13052`, and the 972/2058 decisions must recompute from the database
row by row. The runner enumerates only each v2 `repo_relative_path` and
`wiki_relative_path`. It rejects symlinks, special files, hard links, NOCOW,
alternates, shallow/replace/grafts/worktrees, promisor state, unknown object
material and incomplete pack/index pairs. One preloaded mount inventory also
guards the `objects` and `refs` roots plus every traversed directory and file:
all must remain on the trusted snapshot device, outside nested mountpoints and,
for directories, outside nested Btrfs subvolumes. Traversal errors fail closed.
Forbidden-path probes (`objects/info/alternates`, grafts, replace refs and the
other listed sentinels) lstat each path component without following links and
validate every ancestor before probing its child; a symlink or mount boundary
therefore stops the plan before any lookup can escape the snapshot tree.
Only three exact derived-file classes observed in the bounded 45-repository
inventory are excluded: dumb-HTTP `objects/info/packs`,
`objects/pack/pack-<oid>.bitmap` with its exact complete pack/index pair, and
monolithic `objects/info/commit-graph`. Each must be a bounded regular,
single-link, non-NOCOW file. The reference manifest records its exact path,
class and byte size, but the runner neither reads nor copies the excluded
bytes. Unobserved `.rev`, MIDX and split commit-graph layouts stay fail-closed;
`.promisor`, `.mtimes`, `.keep`, unknown and temporary lookalikes are not
classified as disposable accelerators.
The same inventory found receive-pack quarantine directories matching only an
exact direct child `objects/tmp_objdir-incoming-[A-Za-z0-9]{6}`. A match must
retain the reviewed uid/gid `1000:1000`, mode `0755`, single-link and non-NOCOW
directory metadata. Its exact path and lstat fields are recorded, then the
subtree is pruned without reading, hashing, copying or reflinking contents; the
sealed read-only incident snapshot retains the forensic copy. Any name,
metadata, mount or subvolume drift remains a hard stop. A reviewed reference/OID
manifest is required; raw HEAD/refs files are never copied. The intended data
path is same-filesystem per-file `--reflink=always`, with no byte-copy fallback
and a bounded exclusive-allocation gate.
This slice is intentionally **NO-GO for apply** at the current review point.
`plan` inventories and prints the exact ref-manifest digest, byte length and
canonical compact JSON. The JSON is the reviewable, hash-bound reference/OID
artifact: it contains approved repository identities, ref names/OIDs, object
totals and stat-bound derived/quarantine exclusions, but no Git object bytes,
hooks or config. The same plan prints the canonical unsupported-state report
and a separate incident-closure report, each with its SHA-256, byte length and
compact ASCII JSON. The closure report is evidence only: its disposition keeps
`expectedSha256` and `expectedBytes` null and retains an explicit review-pin
blocker.
The reviewed plan evidence bound by the disposition is reference manifest
`9cddaf0e…a311` (36,010 bytes), unsupported-state report
`4b2cecf8…106ac` (119,468 bytes), and schema catalog
`b5e3b677…b6db`. The report has exact 45 repository rows with empty
`schema_missing`, `schema_mismatch`, and `anomalies` arrays. These facts close
only the owner's material-state decision; fsck/reachability, physical blob
inventory, sanitized archive creation and candidate acceptance remain separate
verifiers.
The unsupported-state report has one deterministic row for every one of the
45 kept repository IDs, including explicit zeroes. It separates direct
repository-relation counts, directional base/head PR counts, repo-unit type
counts, text-metadata presence booleans and denormalized numeric hints; it does
not add overlapping relations into a misleading grand total. LFS rows are
reported per repository with validated logical byte sums and distinct-OID
aggregates, including size conflicts and sharing with non-kept repositories.
Every association sharing a kept OID must also have an integer repository ID
that resolves to the exact snapshot `repository` table; invalid and orphan
owners make shared/unique byte aggregates unavailable and add a data-anomaly
blocker.
Attachments are reported per repository with validated logical byte sums and
explicit, overlapping issue/comment/release/unlinked link splits. Neither
category claims that a physical blob exists; no title, body, note, comment,
config, URL, token, key, secret, hash payload or blob content is selected or
exported.
The report also embeds the exact `PRAGMA table_xinfo` catalog and explicit
label-to-table-to-repository-column map for a code-owned allowlist of relevant
tables. `sqlite_master` must prove an ordinary, non-virtual table before any
row query; SQLite 3.37+ adds `PRAGMA table_list` ordinary-main-table proof. On
an older compatible SQLite, the weaker object-kind capability is recorded and
adds a coverage blocker. `query_only=ON` and `trusted_schema=OFF` are read back
before the snapshot connection is accepted. Only column identifiers and
structural metadata are exported, never DDL, defaults or row values. The v3
closure inventory follows code-owned, schema-attested primary joins for the
issue/PR dependency tables (comments, assignees, content histories, labels, issue
users/watches, reactions, reviews/state, dependencies, notifications,
stopwatches, tracked time and auto-merge), plus projects/boards, releases,
attachments, Packages and Actions. It emits only IDs required to prove joins,
actor classes, counts, declared sizes and SQLite byte lengths; it never selects
or prints text bodies, names, URLs, package metadata, workflow payloads,
credentials, tokens, secrets or logs. Package and Actions target features stay
disabled and no legacy row is importable. The successor additionally closes
every concrete FK-like comment field in the pinned schema (labels, projects,
milestones, tracked time, assignees, dependency, review and cross-reference),
content-history-to-comment, review-team and pull-merger provenance. Target
repository and local actor IDs are classified through the exact 2058/972
decisions. Migrated external-author IDs are never treated as local users and
are represented only by presence counts and author-name byte lengths. A
nonzero comment-assignee or review-reviewer team must resolve to an exact team
and organization; because no organization/team allowlist exists, its row,
team and organization IDs enter a sealed HOLD inventory and add the narrowly
scoped `issue-pr-team-mapping-hold` blocker. Orphans, conflicting user/team
identities, invalid cross-reference pairs and schema/type drift fail closed.
The report is still not a physical-file or sanitized-archive verifier, so
those separate blockers remain explicit.
The schema-catalog digest is recorded as observed evidence but deliberately
remains unpinned as reviewed activation authority. Schema absence/type drift,
invalid numeric/OID state or aggregate overflow is a hard error. `apply` fails
before creating `/volume1/docker/nodedc-gitea` until all of the following are
registered:
- root-owned Btrfs proof that the named snapshot UUID is read-only;
- exact stopped/restart-`no` legacy container image ID, image ref and sole
`/volume1/docker/gitea:/data` bind inspect;
- a separately verified exact reference/OID manifest digest, fsck,
reachability and selected-object reconstruction;
- immutable sanitized archives for issues, PRs, comments, releases, labels,
projects and repository descriptions, with no legacy row import;
- physical attachment inventory and reachable LFS pointer/OID/size/SHA proof;
- kept-user mapping before any collaboration recreation; and
- package/Actions schema plus physical zero closure and target unit-policy
acceptance.
All 25 direct repository relations have explicit dispositions, including the
15 exact zero-and-drop categories. Pull base/head counts remain directional
views of the same five PRs. All repository numeric hints are dropped and
recomputed. Repo-unit types 110 are total: clean types 15 and 8 are recreated
once per repository; external wiki/tracker are forbidden; Packages (9) and
Actions (10) are absent in the target and remain globally disabled. Legacy
unit config is never imported. The topics verifier accepts exact JSON `null`
as semantic empty state, or a canonical JSON array of sorted unique lowercase
names matching `^[a-z0-9][-.a-z0-9]*$` with at most 35 UTF-8 bytes. SQL NULL,
whitespace/case variants, quoted `"null"`, objects, numbers, booleans and
noncanonical arrays fail closed. The current snapshot must prove exactly 45
serialized JSON nulls, zero serialized arrays, zero material topics and zero
`repo_topic` rows; target cache is rebuilt from relations.
The activation hook remains frozen until these blockers are closed and the
full clean-database recreation, reflink reconstruction, doctor/fsck,
credential-table zeroing, rollback and public acceptance path has its own
runtime smoke. The current reviewed source identities are:
- runner `nodedc-deploy` SHA-256
`c766985aa02fe911fa5a873717276f3503d9cecded448e41dc6945030dcbce9e`;
- deterministic builder SHA-256
`b92076583aeab854015a05deb9d942c8dfcf3ca475fd3d52115d402d6f61acca`;
- salvage policy test SHA-256
`99b96b6179286a78e7d53ee6a923fc092e3471a8cece382a472449d455ad212c`;
- fresh-install policy test SHA-256
`20653c640142c25e7d2ef712048acc7889c56e1d519e2d03b5cad4127e48947c`;
- confirmed disposition SHA-256
`0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c`;
- confirmed closure disposition SHA-256
`7ed66d9848268431a703fe24b22c41afbaa7c5ff48949604d6fc448d93e0d243`;
- successor v3 descriptor SHA-256
`9b98eb1a1640fd5569cf051a621837379b167eff4527313a43a0a851e7cc181a`;
- deterministic local review artifact
`nodedc-gitea-gitea-incident-salvage-subrelation-closure-20260814-006.tgz`
SHA-256
`d3e598cf892b1371912dcd9cef64caa991dd23613e48fe979d0f95d493c23d53`.
Only after that exact runner is separately reviewed, promoted to the
root-owned runner path and its installed SHA-256 is re-attested may the exact
artifact be staged into the canonical inbox for a canonical `plan` command.
Staging is evidence collection only; it grants no authority to run `apply`.
The legacy identity is now pinned to the observed ref `gitea/gitea:latest`
and immutable image ID
`sha256:bf95d9a45ce4fe38b027d051cdc4a4bc531513489fa6244af4074efbb1c376d6`.
The mutable tag is provenance only. Every plan re-inspects exact name
`/gitea`, stopped state, restart `no`, and the sole RW bind
`/volume1/docker/gitea:/data`; drift is a hard stop.
The successor plan must report these exact remaining blockers:
- `attachment-physical-verifier-pending`;
- `candidate-root-activation-hard-frozen`;
- `closure-report-review-pin-pending`;
- `collaboration-kept-user-mapping-verifier-pending`;
- `forensic-ref-archive-verifier-pending`;
- `issue-pr-metadata-sanitized-archive-verifier-pending`;
- `lfs-reachable-pointer-physical-verifier-pending`;
- `package-action-physical-closure-verifier-pending`;
- `reference-manifest-fsck-reachability-verifier-pending`;
- `repository-object-reconstruction-verifier-pending`;
- `target-unit-policy-acceptance-pending`;
- `unsupported-schema-catalog-verifier-pending`.
Any other blocker or identity drift is also a stop. `apply` remains prohibited
and hard-frozen before root creation until a subsequent reviewed runner
revision closes every blocker and implements and smoke-tests the complete
activation/rollback path. The successor files must be independently reviewed
before any runner promotion or inbox staging.
Run the bounded policy suite with:
```bash
PYTHONDONTWRITEBYTECODE=1 \
python3 infra/deploy-runner/test_gitea_incident_salvage.py -v
```
The frozen local cutoff passes 54 incident-salvage tests and 26 fresh-install
tests, plus Python compilation and `git diff --check`.
`n8n-private-extension` is a staging-only trust boundary for reviewed offline
n8n private-node releases. Its artifact may contain exactly one digest-bound
`n8n-nodes-ndc` release with `package.tgz`, `release.json` and
@@ -0,0 +1,222 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
cp,
lstat,
mkdir,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const fixtureRoot = resolve(scriptDir, "fixtures/gitea");
const artifactDir = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|| resolve(scriptDir, "../deploy-artifacts"),
);
const [patchId = "gitea-fresh-install-20260813-001", ...extra] =
process.argv.slice(2);
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
throw new Error("usage: build-gitea-fresh-install-artifact.mjs [patch-id]");
}
const composeRelative = "docker-compose.gitea.yml";
const descriptorRelative = "deployment/gitea-fresh-install-v1.json";
const files = [composeRelative, descriptorRelative];
const stage = await mkdtemp(join(tmpdir(), "nodedc-gitea-artifact-"));
const payload = join(stage, "payload");
const target = join(artifactDir, `nodedc-gitea-${patchId}.tgz`);
await assertFixtureContract();
try {
await mkdir(payload, { recursive: true });
for (const relative of files) {
const source = resolve(fixtureRoot, relative);
const sourceStat = await lstat(source);
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
throw new Error(`gitea_fixture_type_rejected:${relative}`);
}
const destination = join(payload, relative);
await mkdir(dirname(destination), { recursive: true });
await cp(source, destination, { force: true, verbatimSymlinks: true });
}
await writeFile(
join(stage, "manifest.env"),
`id=${patchId}\ncomponent=gitea\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8");
await mkdir(artifactDir, { recursive: true });
const tar = spawnSync(
"python3",
["-c", canonicalTarScript(), target, stage],
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
);
if (tar.status !== 0) {
throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
}
const digest = sha256(await readFile(target));
console.log(JSON.stringify({
ok: true,
patchId,
artifact: target,
sha256: digest,
component: "gitea",
entries: files,
services: ["gitea"],
image: "docker.gitea.com/gitea:1.27.1-rootless@sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148",
installMode: "fresh-only",
database: "fresh-sqlite-only",
lfs: "disabled-pending-reviewed-restore-transition",
transport: "unix:/run/gitea/gitea.sock",
networkMode: "none",
minimumComposeVersion: "2.20.1",
preserved: ["legacy-gitea-root-unread-and-untouched"],
excluded: [
"secrets",
"runtime-data",
"database",
"repositories",
"users",
"tokens",
"ssh-keys",
"hooks",
],
}, null, 2));
} finally {
await rm(stage, { recursive: true, force: true });
}
async function assertFixtureContract() {
const composeBytes = await readFile(resolve(fixtureRoot, composeRelative));
const compose = composeBytes.toString("utf8");
const descriptor = JSON.parse(
await readFile(resolve(fixtureRoot, descriptorRelative), "utf8"),
);
const expectedImage =
"docker.gitea.com/gitea:1.27.1-rootless@sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148";
const required = [
`image: ${expectedImage}`,
"platform: linux/amd64",
"pull_policy: never",
"network_mode: none",
'user: "1000:1000"',
"stop_grace_period: 30s",
"driver: json-file",
'max-size: "10m"',
'max-file: "3"',
"GITEA__server__PROTOCOL: http+unix",
"GITEA__server__HTTP_ADDR: /run/gitea/gitea.sock",
'GITEA__server__UNIX_SOCKET_PERMISSION: "0666"',
"GITEA__server__LOCAL_ROOT_URL: http://unix/",
'GITEA__server__DISABLE_SSH: "true"',
'GITEA__server__LFS_START_SERVER: "false"',
'GITEA__server__LFS_ALLOW_PURE_SSH: "false"',
"GITEA__security__SECRET_KEY_URI: file:/run/secrets/gitea_secret_key",
"GITEA__security__INTERNAL_TOKEN_URI: file:/run/secrets/gitea_internal_token",
"GITEA__security__TWO_FACTOR_AUTH: enforced",
'GITEA__security__REVERSE_PROXY_LIMIT: "1"',
"GITEA__security__ALLOWED_HOST_LIST: loopback",
"GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 127.0.0.0/8,::1/128",
'GITEA__service__DISABLE_REGISTRATION: "true"',
'GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION: "false"',
'GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION_API: "false"',
'GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "false"',
'GITEA__service__ENABLE_BASIC_AUTHENTICATION: "false"',
'GITEA__admin__DISABLE_REGULAR_ORG_CREATION: "true"',
"GITEA__admin__USER_DISABLED_FEATURES: deletion,manage_ssh_keys,manage_gpg_keys,change_username",
'GITEA__security__DISABLE_GIT_HOOKS: "true"',
'GITEA__security__DISABLE_WEBHOOKS: "true"',
'GITEA__repository__DISABLE_MIGRATIONS: "true"',
'GITEA__packages__ENABLED: "false"',
'GITEA__oauth2__ENABLED: "false"',
'GITEA__openid__ENABLE_OPENID_SIGNIN: "false"',
'GITEA__cron.update_checker__ENABLED: "false"',
"source: /volume1/docker/nodedc-gitea/socket",
"target: /run/gitea",
"create_host_path: false",
"read_only: true",
"no-new-privileges:true",
];
for (const fragment of required) {
if (!compose.includes(fragment)) {
throw new Error(`gitea_compose_boundary_missing:${fragment}`);
}
}
for (const forbidden of [
"4022",
"2222:2222",
"0.0.0.0:3000",
"ports:",
"networks:",
"/var/run/docker.sock",
"/volume1/docker/gitea",
"privileged: true",
"pull_policy: always",
"__FILE",
"GITEA__security__SECRET_KEY:",
"GITEA__security__INTERNAL_TOKEN:",
"GITEA__server__LFS_JWT_SECRET:",
"GITEA__server__LFS_JWT_SECRET_URI",
"gitea_lfs_jwt_secret",
"lfs-jwt-secret",
"GITEA__server__REVERSE_PROXY_LIMIT",
"GITEA__server__REVERSE_PROXY_TRUSTED_PROXIES",
"GITEA__security__ENABLE_REVERSE_PROXY_AUTHENTICATION",
"GITEA__security__ENABLE_REVERSE_PROXY_AUTHENTICATION_API",
"GITEA__security__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION",
"GITEA__service__DISABLE_REGULAR_ORG_CREATION",
"GITEA__service__USER_DISABLED_FEATURES",
]) {
if (compose.includes(forbidden)) {
throw new Error(`gitea_compose_boundary_violation:${forbidden}`);
}
}
if (
descriptor.schemaVersion !== "nodedc.gitea.fresh-install.v1"
|| descriptor.action !== "fresh-install"
|| descriptor.component !== "gitea"
|| descriptor.compose?.sha256 !== sha256(composeBytes)
|| descriptor.runtime?.image !== expectedImage
|| descriptor.runtime?.minimumComposeVersion !== "2.20.1"
|| descriptor.runtime?.lfs !== "disabled-pending-reviewed-restore-transition"
|| descriptor.runtime?.transport !== "unix:/run/gitea/gitea.sock"
|| descriptor.runtime?.networkMode !== "none"
|| descriptor.runtime?.logging !== "bounded-json-file-10m-x3"
|| descriptor.runtime?.stopGracePeriod !== "30s"
|| descriptor.trust?.artifactSecrets !== "forbidden"
|| descriptor.trust?.legacyRootAccess !== "forbidden"
) {
throw new Error("gitea_descriptor_contract_mismatch");
}
}
function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'wb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
@@ -0,0 +1,322 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
cp,
link,
lstat,
mkdir,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const fixtureRoot = resolve(scriptDir, "fixtures/gitea-salvage");
const decisionRoot = resolve(
scriptDir,
"../../../security-incidents/gitea-20260814/confirmed-decisions-v2",
);
const artifactDir = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|| resolve(scriptDir, "../deploy-artifacts"),
);
const [patchId = "gitea-incident-salvage-subrelation-closure-20260814-006", ...extra] =
process.argv.slice(2);
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
throw new Error(
"usage: build-gitea-incident-salvage-artifact.mjs [patch-id]",
);
}
const composeRelative = "docker-compose.gitea.yml";
const descriptorRelative = "deployment/gitea-incident-salvage-v3.json";
const decisionPrefix = "deployment/gitea-incident-salvage";
const dispositionRelative = `${decisionPrefix}/confirmed-disposition-v1.json`;
const closureDispositionRelative =
`${decisionPrefix}/confirmed-closure-disposition-v1.json`;
const descriptorSha256 =
"9b98eb1a1640fd5569cf051a621837379b167eff4527313a43a0a851e7cc181a";
const dispositionSha256 =
"0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c";
const closureDispositionSha256 =
"7ed66d9848268431a703fe24b22c41afbaa7c5ff48949604d6fc448d93e0d243";
const decisionFiles = [
["confirmed-decision.json", "dc9528462624158eb44218d37cc7054d551ca2d7ded562592982aa3f34c9fc2a"],
["users.decisions.csv", "e3b82f1073a86eea9e567edff062dd1689d21ec0edcf3ed92e844da9351ee8b6"],
["repositories.decisions.csv", "76b4bae2ab5cec490330c19bfc5ae9429abf7636705ae028c1c64fd54a6a0493"],
];
const files = [
composeRelative,
descriptorRelative,
dispositionRelative,
closureDispositionRelative,
...decisionFiles.map(([name]) => `${decisionPrefix}/${name}`),
];
const stage = await mkdtemp(join(tmpdir(), "nodedc-gitea-salvage-artifact-"));
const payload = join(stage, "payload");
const target = join(artifactDir, `nodedc-gitea-${patchId}.tgz`);
const targetTemporary = join(
artifactDir,
`.${basename(target)}.${process.pid}.tmp`,
);
await assertSourceContract();
try {
await mkdir(payload, { recursive: true });
for (const relative of [
composeRelative,
descriptorRelative,
dispositionRelative,
closureDispositionRelative,
]) {
await copyRegularFile(resolve(fixtureRoot, relative), join(payload, relative), relative);
}
for (const [name] of decisionFiles) {
await copyRegularFile(
resolve(decisionRoot, name),
join(payload, decisionPrefix, name),
`${decisionPrefix}/${name}`,
);
}
await writeFile(
join(stage, "manifest.env"),
`id=${patchId}\ncomponent=gitea\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8");
await mkdir(artifactDir, { recursive: true });
const tar = spawnSync(
"python3",
["-c", canonicalTarScript(), targetTemporary, stage],
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
);
if (tar.status !== 0) {
throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
}
try {
await link(targetTemporary, target);
} catch (error) {
if (error?.code === "EEXIST") {
throw new Error("gitea_salvage_artifact_target_already_exists");
}
throw error;
}
console.log(JSON.stringify({
ok: true,
patchId,
artifact: target,
sha256: sha256(await readFile(target)),
component: "gitea",
transition: "clean-state-incident-salvage",
entries: files,
services: ["gitea"],
image: "docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c",
decisions: {
users: { active: 2, locked: 8, delete: 962 },
repositories: { keep: 45, delete: 2013 },
},
disposition: {
sha256: dispositionSha256,
forensicRefs: 105,
liveRefs: 93,
archiveOnlyRefs: 12,
},
closureDisposition: {
sha256: closureDispositionSha256,
state: "plan-report-review-pending",
},
stateBoundary: "new-database-config-secrets-identities",
repositoryBoundary: "exact-45-object-and-validated-ref-material-only",
networkMode: "none",
transport: "unix:/run/gitea/gitea.sock",
stagePolicy: "after-exact-runner-promotion-plan-only",
applyPolicy: "hard-frozen-before-root-creation",
}, null, 2));
} finally {
await rm(targetTemporary, { force: true });
await rm(stage, { recursive: true, force: true });
}
async function copyRegularFile(source, destination, label) {
const sourceStat = await lstat(source);
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
throw new Error(`gitea_salvage_source_type_rejected:${label}`);
}
await mkdir(dirname(destination), { recursive: true });
await cp(source, destination, { force: true, verbatimSymlinks: true });
}
function canonicalJson(value) {
if (Array.isArray(value)) {
return value.map(canonicalJson);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]),
);
}
return value;
}
async function assertSourceContract() {
for (const [name, expected] of decisionFiles) {
const bytes = await readFile(resolve(decisionRoot, name));
if (sha256(bytes) !== expected) {
throw new Error(`gitea_salvage_decision_digest_mismatch:${name}`);
}
}
const composeBytes = await readFile(resolve(fixtureRoot, composeRelative));
const compose = composeBytes.toString("utf8");
const descriptorBytes = await readFile(
resolve(fixtureRoot, descriptorRelative),
);
const descriptor = JSON.parse(descriptorBytes.toString("utf8"));
if (sha256(descriptorBytes) !== descriptorSha256) {
throw new Error("gitea_salvage_descriptor_digest_mismatch");
}
const dispositionBytes = await readFile(
resolve(fixtureRoot, dispositionRelative),
);
const disposition = JSON.parse(dispositionBytes.toString("utf8"));
if (sha256(dispositionBytes) !== dispositionSha256) {
throw new Error("gitea_salvage_disposition_digest_mismatch");
}
const canonicalDisposition = Buffer.from(
`${JSON.stringify(canonicalJson(disposition))}\n`,
"utf8",
);
if (!dispositionBytes.equals(canonicalDisposition)) {
throw new Error("gitea_salvage_disposition_not_canonical");
}
const closureDispositionBytes = await readFile(
resolve(fixtureRoot, closureDispositionRelative),
);
const closureDisposition = JSON.parse(closureDispositionBytes.toString("utf8"));
if (sha256(closureDispositionBytes) !== closureDispositionSha256) {
throw new Error("gitea_salvage_closure_disposition_digest_mismatch");
}
const canonicalClosureDisposition = Buffer.from(
`${JSON.stringify(canonicalJson(closureDisposition))}\n`,
"utf8",
);
if (!closureDispositionBytes.equals(canonicalClosureDisposition)) {
throw new Error("gitea_salvage_closure_disposition_not_canonical");
}
const expectedImage =
"docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c";
for (const required of [
`image: ${expectedImage}`,
"platform: linux/amd64",
"pull_policy: never",
"network_mode: none",
'user: "1000:1000"',
"entrypoint:",
"- /usr/local/bin/gitea",
"- /etc/gitea/app.ini",
"read_only: true",
"cap_drop:",
"- ALL",
"no-new-privileges:true",
"source: /volume1/docker/nodedc-gitea/data",
"target: /data",
"source: /volume1/docker/nodedc-gitea/config",
"target: /etc/gitea",
"source: /volume1/docker/nodedc-gitea/socket",
"target: /run/gitea",
"create_host_path: false",
]) {
if (!compose.includes(required)) {
throw new Error(`gitea_salvage_compose_boundary_missing:${required}`);
}
}
for (const forbidden of [
"ports:",
"networks:",
"/var/run/docker.sock",
"/volume1/docker/gitea",
"privileged: true",
"4022",
"TWO_FACTOR_AUTH",
"LFS_JWT_SECRET",
]) {
if (compose.includes(forbidden)) {
throw new Error(`gitea_salvage_compose_boundary_violation:${forbidden}`);
}
}
if (
descriptor.schemaVersion !== "nodedc.gitea.incident-salvage.v3"
|| descriptor.action !== "clean-state-salvage"
|| descriptor.compose?.sha256 !== sha256(composeBytes)
|| descriptor.runtime?.image !== expectedImage
|| descriptor.runtime?.networkMode !== "none"
|| descriptor.runtime?.database !== "new-sqlite-1.27.2-only"
|| descriptor.disposition?.file !== dispositionRelative
|| descriptor.disposition?.sha256 !== dispositionSha256
|| descriptor.closureDisposition?.file !== closureDispositionRelative
|| descriptor.closureDisposition?.sha256 !== closureDispositionSha256
|| descriptor.closureDisposition?.predecessorArtifactSha256
!== "d6870b5583a2f329eadb4e6cda65fdf4d271532df5ffbf8bfb1403968a434672"
|| descriptor.trust?.legacyDatabaseImported !== false
|| descriptor.trust?.legacyCredentialsImported !== false
|| descriptor.identity?.twoFactorAuthentication
!== "not-configured-by-transition"
) {
throw new Error("gitea_salvage_descriptor_contract_mismatch");
}
if (
disposition.schemaVersion !== "nodedc.gitea.incident-disposition.v1"
|| disposition.incidentId !== "gitea-20260814"
|| disposition.activation?.allowedOperation !== "canonical-plan-only"
|| disposition.activation?.applyFrozen !== true
|| disposition.activation?.freezeBoundary
!== "before-candidate-root-creation"
|| disposition.referencePolicy?.forensicScope?.allDiscoveredRefs !== 105
|| disposition.referencePolicy?.exactDecisions?.length !== 105
|| disposition.referencePolicy?.liveRestore?.totalRefs !== 93
|| disposition.referencePolicy?.archiveOnly?.totalRefs !== 12
) {
throw new Error("gitea_salvage_disposition_contract_mismatch");
}
if (
closureDisposition.schemaVersion
!== "nodedc.gitea.incident-closure-disposition.v1"
|| closureDisposition.incidentId !== "gitea-20260814"
|| closureDisposition.activation?.allowedOperation !== "canonical-plan-only"
|| closureDisposition.activation?.applyFrozen !== true
|| closureDisposition.closureReport?.expectedSha256 !== null
|| closureDisposition.predecessor?.dispositionSha256 !== dispositionSha256
|| closureDisposition.remainingBlockers
?.includes("closure-report-review-pin-pending") !== true
) {
throw new Error("gitea_salvage_closure_disposition_contract_mismatch");
}
}
function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'xb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Deterministic data-only declaration for the registered NAS map access domain."""
import argparse
import gzip
import hashlib
import io
import json
import re
import tarfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def build(patch, state="enabled"):
if not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", patch) or state not in ("enabled", "disabled"):
raise ValueError("Invalid map access release")
descriptor = json.loads((ROOT / "deployment/mission-core-map-access/access.json").read_text())
if descriptor != {"schemaVersion": "nodedc.mission-core-map-access.v1", "state": "enabled"}:
raise ValueError("Unexpected source contract")
descriptor["state"] = state
members = {
"manifest.env": f"id={patch}\ncomponent=mission-core-map-access\ntype=app-overlay\n".encode(),
"files.txt": b"access.json\n",
"payload/access.json": (json.dumps(descriptor, sort_keys=True, separators=(",", ":")) + "\n").encode(),
}
result = io.BytesIO()
with gzip.GzipFile(fileobj=result, mode="wb", filename="", mtime=0) as compressed:
with tarfile.open(fileobj=compressed, mode="w", format=tarfile.USTAR_FORMAT) as archive:
for name, content in members.items():
member = tarfile.TarInfo(name); member.mode = 0o644; member.size = len(content)
archive.addfile(member, io.BytesIO(content))
return result.getvalue()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("patch")
parser.add_argument("--state", choices=("enabled", "disabled"), default="enabled")
args = parser.parse_args()
raw = build(args.patch, args.state)
target = ROOT / "infra/deploy-artifacts" / ("nodedc-" + args.patch + ".tgz")
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("xb") as output:
output.write(raw)
digest = hashlib.sha256(raw).hexdigest()
target.with_suffix(target.suffix + ".sha256").write_text(digest + " " + target.name + "\n")
print(json.dumps({"artifact": str(target), "sha256": digest, "bytes": len(raw)}))
@@ -0,0 +1,126 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cp, lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(scriptDir, "../..");
const taskerRoot = resolve(
process.env.NODEDC_TASKMANAGER_ROOT || resolve(platformRoot, "../../data/dc_taskmanager/NODEDC_TASKMANAGER"),
);
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
const [release = "20260829-001", ...extra] = process.argv.slice(2);
if (extra.length || release !== "20260829-001") {
throw new Error("usage: build-tasker-attachment-formats-artifact.mjs [20260829-001]");
}
const descriptor = {
artifactBasename: `nodedc-tasker-attachment-formats-${release}.tgz`,
component: "tasker",
expectedCommit: "f9308539bfe71e4e2359c47ed5c230fa1fb386c9",
files: [
"plane-src/apps/api/plane/settings/common.py",
"plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx",
"plane-src/apps/web/core/components/issues/peek-overview/view.tsx",
"plane-src/apps/web/styles/globals.css",
"plane-src/packages/services/src/file/helper.ts",
],
patchId: `tasker-attachment-formats-${release}`,
sourceRoot: taskerRoot,
};
const sourceCommit = gitOutput(descriptor.sourceRoot, ["rev-parse", "HEAD"]);
if (sourceCommit !== descriptor.expectedCommit) {
throw new Error(`source_commit_mismatch:${descriptor.component}:${sourceCommit}`);
}
const sourceStatus = gitOutput(descriptor.sourceRoot, ["status", "--porcelain"]);
if (sourceStatus) throw new Error(`source_worktree_not_clean:${descriptor.component}`);
await mkdir(artifactDir, { recursive: true });
const stage = await mkdtemp(join(tmpdir(), "nodedc-tasker-attachment-formats-"));
const payload = join(stage, "payload");
const artifact = join(artifactDir, descriptor.artifactBasename);
try {
await assertFresh(artifact);
await mkdir(payload, { recursive: true });
for (const relativePath of descriptor.files) {
const source = resolve(descriptor.sourceRoot, relativePath);
const sourceStat = await lstat(source);
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
throw new Error(`source_file_rejected:${descriptor.component}:${relativePath}`);
}
const destination = join(payload, relativePath);
await mkdir(dirname(destination), { recursive: true });
await cp(source, destination, { force: false, verbatimSymlinks: true });
}
await writeFile(
join(stage, "manifest.env"),
`id=${descriptor.patchId}\ncomponent=${descriptor.component}\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${descriptor.files.join("\n")}\n`, "utf8");
const tar = spawnSync("python3", ["-c", canonicalTarScript(), artifact, stage], {
encoding: "utf8",
maxBuffer: 128 * 1024 * 1024,
});
if (tar.status !== 0) throw new Error(`tar_failed:${descriptor.component}:${tar.stderr || tar.stdout}`);
const sha256 = createHash("sha256").update(await readFile(artifact)).digest("hex");
console.log(
JSON.stringify(
{
ok: true,
release,
artifact,
component: descriptor.component,
files: descriptor.files,
patchId: descriptor.patchId,
sha256,
sourceCommit,
},
null,
2,
),
);
} finally {
await rm(stage, { recursive: true, force: true });
}
function gitOutput(cwd, args) {
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
if (result.status !== 0) throw new Error(`git_failed:${cwd}:${args.join("_")}:${result.stderr || result.stdout}`);
return result.stdout.trim();
}
async function assertFresh(path) {
try {
await lstat(path);
} catch (error) {
if (error?.code === "ENOENT") return;
throw error;
}
throw new Error(`output_already_exists:${path}`);
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'wb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
@@ -0,0 +1,112 @@
{
"schemaVersion": "nodedc.gitea.incident-salvage.v1",
"action": "clean-state-salvage",
"component": "gitea",
"incidentId": "gitea-20260814",
"installRoot": "/volume1/docker/nodedc-gitea",
"compose": {
"file": "docker-compose.gitea.yml",
"project": "nodedc-gitea",
"service": "gitea",
"sha256": "2f031d5bfff4f42c73cabd8c94487ec3e4f1e1b0a96d3b7eec958904f735908a"
},
"runtime": {
"image": "docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c",
"imageId": "sha256:272085a806e6d182352cdb011c0ebab1d2efc7ec45247de84de5659c7bc5c4c6",
"repoDigest": "docker.gitea.com/gitea@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c",
"platform": "linux/amd64",
"pullPolicy": "never",
"entrypoint": "/usr/local/bin/gitea",
"runAs": "1000:1000",
"transport": "unix:/run/gitea/gitea.sock",
"networkMode": "none",
"ssh": "disabled-no-published-port",
"lfs": "unsupported-hard-stop",
"database": "new-sqlite-1.27.2-only",
"logging": "bounded-json-file-10m-x3",
"stopGracePeriod": "30s",
"candidateRestartPolicy": "no",
"acceptedRestartPolicy": "unless-stopped"
},
"decision": {
"schema": "nodedc.gitea.incident-decision/v2",
"manifestFile": "deployment/gitea-incident-salvage/confirmed-decision.json",
"manifestSha256": "dc9528462624158eb44218d37cc7054d551ca2d7ded562592982aa3f34c9fc2a",
"usersFile": "deployment/gitea-incident-salvage/users.decisions.csv",
"usersSha256": "e3b82f1073a86eea9e567edff062dd1689d21ec0edcf3ed92e844da9351ee8b6",
"repositoriesFile": "deployment/gitea-incident-salvage/repositories.decisions.csv",
"repositoriesSha256": "76b4bae2ab5cec490330c19bfc5ae9429abf7636705ae028c1c64fd54a6a0493",
"users": {
"active": 2,
"locked": 8,
"delete": 962
},
"repositories": {
"keep": 45,
"delete": 2013,
"dctouchPrivate": 32,
"silverPublic": 13
}
},
"snapshot": {
"root": "/volume1/.nodedc-security-snapshots/docker-gitea-incident-20260814",
"uuid": "f5a3fe3a-93ea-bb4d-847f-6221a6bcbc9f",
"readOnlyRequired": true,
"database": "gitea/gitea/gitea.db",
"databaseBytes": 182681600,
"databaseSha256": "8db9e74a5641662a808d8252c5d6c9de43fe9efd8687634bfbff2d4361a13052",
"repositoriesRoot": "gitea/git/repositories"
},
"trust": {
"legacyRootMounted": false,
"legacyDatabaseImported": false,
"legacyConfigImported": false,
"legacyCredentialsImported": false,
"artifactSecrets": "forbidden",
"runtimeSecrets": "runner-generated",
"repositorySelection": "exact-v2-rows-only",
"repositoryCopy": "same-filesystem-per-file-reflink-object-and-validated-ref-material-only",
"forbiddenRepositoryMaterial": [
"hooks",
"config",
"alternates",
"http-alternates",
"shallow",
"replace",
"grafts",
"worktrees",
"commondir",
"lfs"
],
"unsupportedDatabaseState": "zero-material-rows-required-before-apply"
},
"identity": {
"preserveNumericUserIds": false,
"preserveNumericRepositoryIds": false,
"oldToNewIdMapping": "/volume1/docker/nodedc-gitea/audit/identity-map.json",
"emails": "new-local-noreply-only",
"passwordHashes": "never-imported",
"activeBootstrapCredentials": "root-only-runner-state-require-change",
"lockedCredentials": "fresh-random-discarded-and-login-prohibited",
"twoFactorAuthentication": "not-configured-by-transition"
},
"externalPrerequisites": {
"legacyContainer": "exact-inspect-pin-required-stopped-restart-no",
"publicProxy": "reviewed-nginx-uds-bridge-and-dsm-loopback-upstream",
"firewall": "legacy-isolation-retained-and-loopback-3000-ready",
"candidatePublicationGate": "socket-parent-uid1000-gid1023-mode0700",
"publishAction": "runner-chmod-socket-parent-0750-after-internal-acceptance",
"mustPassBeforeApply": true
},
"acceptance": {
"users": "exact-10-row-by-row",
"repositories": "exact-45-row-by-row-and-visibility",
"credentialsSessionsKeysWebhooksActions": "zero",
"git": "fsck-all-and-public-smart-http-smoke",
"registration": "closed",
"ssh": "disabled",
"network": "none-no-ports",
"publicHttps": "required-before-ledger-success"
},
"rollback": "stop-remove-candidate-prove-absent-quarantine-new-root-never-start-legacy"
}
@@ -0,0 +1,117 @@
{
"schemaVersion": "nodedc.gitea.incident-salvage.v2",
"action": "clean-state-salvage",
"component": "gitea",
"incidentId": "gitea-20260814",
"installRoot": "/volume1/docker/nodedc-gitea",
"compose": {
"file": "docker-compose.gitea.yml",
"project": "nodedc-gitea",
"service": "gitea",
"sha256": "2f031d5bfff4f42c73cabd8c94487ec3e4f1e1b0a96d3b7eec958904f735908a"
},
"runtime": {
"image": "docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c",
"imageId": "sha256:272085a806e6d182352cdb011c0ebab1d2efc7ec45247de84de5659c7bc5c4c6",
"repoDigest": "docker.gitea.com/gitea@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c",
"platform": "linux/amd64",
"pullPolicy": "never",
"entrypoint": "/usr/local/bin/gitea",
"runAs": "1000:1000",
"transport": "unix:/run/gitea/gitea.sock",
"networkMode": "none",
"ssh": "disabled-no-published-port",
"lfs": "disabled-pending-reachable-pointer-physical-sha-verifier",
"database": "new-sqlite-1.27.2-only",
"logging": "bounded-json-file-10m-x3",
"stopGracePeriod": "30s",
"candidateRestartPolicy": "no",
"acceptedRestartPolicy": "unless-stopped"
},
"decision": {
"schema": "nodedc.gitea.incident-decision/v2",
"manifestFile": "deployment/gitea-incident-salvage/confirmed-decision.json",
"manifestSha256": "dc9528462624158eb44218d37cc7054d551ca2d7ded562592982aa3f34c9fc2a",
"usersFile": "deployment/gitea-incident-salvage/users.decisions.csv",
"usersSha256": "e3b82f1073a86eea9e567edff062dd1689d21ec0edcf3ed92e844da9351ee8b6",
"repositoriesFile": "deployment/gitea-incident-salvage/repositories.decisions.csv",
"repositoriesSha256": "76b4bae2ab5cec490330c19bfc5ae9429abf7636705ae028c1c64fd54a6a0493",
"users": {
"active": 2,
"locked": 8,
"delete": 962
},
"repositories": {
"keep": 45,
"delete": 2013,
"dctouchPrivate": 32,
"silverPublic": 13
}
},
"disposition": {
"schema": "nodedc.gitea.incident-disposition.v1",
"file": "deployment/gitea-incident-salvage/confirmed-disposition-v1.json",
"sha256": "0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c"
},
"snapshot": {
"root": "/volume1/.nodedc-security-snapshots/docker-gitea-incident-20260814",
"uuid": "f5a3fe3a-93ea-bb4d-847f-6221a6bcbc9f",
"readOnlyRequired": true,
"database": "gitea/gitea/gitea.db",
"databaseBytes": 182681600,
"databaseSha256": "8db9e74a5641662a808d8252c5d6c9de43fe9efd8687634bfbff2d4361a13052",
"repositoriesRoot": "gitea/git/repositories"
},
"trust": {
"legacyRootMounted": false,
"legacyDatabaseImported": false,
"legacyConfigImported": false,
"legacyCredentialsImported": false,
"artifactSecrets": "forbidden",
"runtimeSecrets": "runner-generated",
"repositorySelection": "exact-v2-rows-only",
"repositoryCopy": "same-filesystem-per-file-reflink-object-and-validated-ref-material-only",
"forbiddenRepositoryMaterial": [
"hooks",
"config",
"alternates",
"http-alternates",
"shallow",
"replace",
"grafts",
"worktrees",
"commondir",
"lfs"
],
"unsupportedDatabaseState": "confirmed-disposition-bound-no-legacy-row-import-verifiers-pending"
},
"identity": {
"preserveNumericUserIds": false,
"preserveNumericRepositoryIds": false,
"oldToNewIdMapping": "/volume1/docker/nodedc-gitea/audit/identity-map.json",
"emails": "new-local-noreply-only",
"passwordHashes": "never-imported",
"activeBootstrapCredentials": "root-only-runner-state-require-change",
"lockedCredentials": "fresh-random-discarded-and-login-prohibited",
"twoFactorAuthentication": "not-configured-by-transition"
},
"externalPrerequisites": {
"legacyContainer": "exact-inspect-pin-required-stopped-restart-no",
"publicProxy": "reviewed-nginx-uds-bridge-and-dsm-loopback-upstream",
"firewall": "legacy-isolation-retained-and-loopback-3000-ready",
"candidatePublicationGate": "socket-parent-uid1000-gid1023-mode0700",
"publishAction": "runner-chmod-socket-parent-0750-after-internal-acceptance",
"mustPassBeforeApply": true
},
"acceptance": {
"users": "exact-10-row-by-row",
"repositories": "exact-45-row-by-row-and-visibility",
"credentialsSessionsKeysWebhooksActions": "zero",
"git": "fsck-all-and-public-smart-http-smoke",
"registration": "closed",
"ssh": "disabled",
"network": "none-no-ports",
"publicHttps": "required-before-ledger-success"
},
"rollback": "stop-remove-candidate-prove-absent-quarantine-new-root-never-start-legacy"
}
@@ -0,0 +1,123 @@
{
"schemaVersion": "nodedc.gitea.incident-salvage.v3",
"action": "clean-state-salvage",
"component": "gitea",
"incidentId": "gitea-20260814",
"installRoot": "/volume1/docker/nodedc-gitea",
"compose": {
"file": "docker-compose.gitea.yml",
"project": "nodedc-gitea",
"service": "gitea",
"sha256": "2f031d5bfff4f42c73cabd8c94487ec3e4f1e1b0a96d3b7eec958904f735908a"
},
"runtime": {
"image": "docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c",
"imageId": "sha256:272085a806e6d182352cdb011c0ebab1d2efc7ec45247de84de5659c7bc5c4c6",
"repoDigest": "docker.gitea.com/gitea@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c",
"platform": "linux/amd64",
"pullPolicy": "never",
"entrypoint": "/usr/local/bin/gitea",
"runAs": "1000:1000",
"transport": "unix:/run/gitea/gitea.sock",
"networkMode": "none",
"ssh": "disabled-no-published-port",
"lfs": "disabled-pending-reachable-pointer-physical-sha-verifier",
"database": "new-sqlite-1.27.2-only",
"logging": "bounded-json-file-10m-x3",
"stopGracePeriod": "30s",
"candidateRestartPolicy": "no",
"acceptedRestartPolicy": "unless-stopped"
},
"decision": {
"schema": "nodedc.gitea.incident-decision/v2",
"manifestFile": "deployment/gitea-incident-salvage/confirmed-decision.json",
"manifestSha256": "dc9528462624158eb44218d37cc7054d551ca2d7ded562592982aa3f34c9fc2a",
"usersFile": "deployment/gitea-incident-salvage/users.decisions.csv",
"usersSha256": "e3b82f1073a86eea9e567edff062dd1689d21ec0edcf3ed92e844da9351ee8b6",
"repositoriesFile": "deployment/gitea-incident-salvage/repositories.decisions.csv",
"repositoriesSha256": "76b4bae2ab5cec490330c19bfc5ae9429abf7636705ae028c1c64fd54a6a0493",
"users": {
"active": 2,
"locked": 8,
"delete": 962
},
"repositories": {
"keep": 45,
"delete": 2013,
"dctouchPrivate": 32,
"silverPublic": 13
}
},
"disposition": {
"schema": "nodedc.gitea.incident-disposition.v1",
"file": "deployment/gitea-incident-salvage/confirmed-disposition-v1.json",
"sha256": "0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c"
},
"snapshot": {
"root": "/volume1/.nodedc-security-snapshots/docker-gitea-incident-20260814",
"uuid": "f5a3fe3a-93ea-bb4d-847f-6221a6bcbc9f",
"readOnlyRequired": true,
"database": "gitea/gitea/gitea.db",
"databaseBytes": 182681600,
"databaseSha256": "8db9e74a5641662a808d8252c5d6c9de43fe9efd8687634bfbff2d4361a13052",
"repositoriesRoot": "gitea/git/repositories"
},
"trust": {
"legacyRootMounted": false,
"legacyDatabaseImported": false,
"legacyConfigImported": false,
"legacyCredentialsImported": false,
"artifactSecrets": "forbidden",
"runtimeSecrets": "runner-generated",
"repositorySelection": "exact-v2-rows-only",
"repositoryCopy": "same-filesystem-per-file-reflink-object-and-validated-ref-material-only",
"forbiddenRepositoryMaterial": [
"hooks",
"config",
"alternates",
"http-alternates",
"shallow",
"replace",
"grafts",
"worktrees",
"commondir",
"lfs"
],
"unsupportedDatabaseState": "closure-disposition-bound-plan-report-review-and-verifiers-pending"
},
"identity": {
"preserveNumericUserIds": false,
"preserveNumericRepositoryIds": false,
"oldToNewIdMapping": "/volume1/docker/nodedc-gitea/audit/identity-map.json",
"emails": "new-local-noreply-only",
"passwordHashes": "never-imported",
"activeBootstrapCredentials": "root-only-runner-state-require-change",
"lockedCredentials": "fresh-random-discarded-and-login-prohibited",
"twoFactorAuthentication": "not-configured-by-transition"
},
"externalPrerequisites": {
"legacyContainer": "exact-inspect-pin-required-stopped-restart-no",
"publicProxy": "reviewed-nginx-uds-bridge-and-dsm-loopback-upstream",
"firewall": "legacy-isolation-retained-and-loopback-3000-ready",
"candidatePublicationGate": "socket-parent-uid1000-gid1023-mode0700",
"publishAction": "runner-chmod-socket-parent-0750-after-internal-acceptance",
"mustPassBeforeApply": true
},
"acceptance": {
"users": "exact-10-row-by-row",
"repositories": "exact-45-row-by-row-and-visibility",
"credentialsSessionsKeysWebhooksActions": "zero",
"git": "fsck-all-and-public-smart-http-smoke",
"registration": "closed",
"ssh": "disabled",
"network": "none-no-ports",
"publicHttps": "required-before-ledger-success"
},
"rollback": "stop-remove-candidate-prove-absent-quarantine-new-root-never-start-legacy",
"closureDisposition": {
"file": "deployment/gitea-incident-salvage/confirmed-closure-disposition-v1.json",
"predecessorArtifactSha256": "d6870b5583a2f329eadb4e6cda65fdf4d271532df5ffbf8bfb1403968a434672",
"schema": "nodedc.gitea.incident-closure-disposition.v1",
"sha256": "7ed66d9848268431a703fe24b22c41afbaa7c5ff48949604d6fc448d93e0d243"
}
}
@@ -0,0 +1 @@
{"activation":{"allowedOperation":"canonical-plan-only","applyFrozen":true,"freezeBoundary":"before-candidate-root-creation"},"authority":{"policyScope":"access-collaboration-issue-pr-attachment-release-label-project-unit-package-action-closure","source":"owner-instruction-in-current-incident-thread","state":"confirmed-policy-evidence-review-pending"},"closureReport":{"expectedBytes":null,"expectedSha256":null,"reviewState":"canonical-plan-output-unreviewed","schema":"nodedc.gitea.salvage-closure-inventory/v1"},"incidentId":"gitea-20260814","policies":{"accessCollaboration":{"accessCache":"DROP_RESET_RECOMPUTE","actorEvidence":"old-user-id-mode-and-kept-deleted-class-only","collaboration":"RECREATE_ONLY_KEPT_ACTORS_AFTER_REVIEWED_OLD_TO_NEW_ID_MAP","deletedActors":962,"keptActors":10,"legacyRowsImported":false,"sensitiveUserPayloadExported":false},"attachments":{"databaseManifest":"ID_UUID_RELATIONS_DECLARED_SIZE_ONLY","filenameOrContentExported":false,"legacyRowsImported":false,"physicalDisposition":"VERIFY_BYTES_THEN_SANITIZED_IMMUTABLE_ARCHIVE_ONLY","physicalPresenceClaimed":false},"issuesPullRequestsMetadata":{"archive":"SANITIZED_IMMUTABLE_ARCHIVE_ONLY","legacyRowsImported":false,"payloadExportedInPlan":false,"planEvidence":"PER_REPOSITORY_COUNTS_ACTOR_CLASSES_TEXT_BYTE_LENGTHS_ONLY","subrelationClosure":{"commentHistoryMerger":"SCHEMA_BOUND_EXACT_RELATION_COUNTS_AND_CLASSES","externalAuthors":"PRESENCE_AND_NAME_BYTE_LENGTHS_ONLY_NO_LOCAL_USER_MAPPING","legacyRowsImported":false,"teamRelations":"EXACT_ROW_TEAM_ORG_IDS_SEALED_HOLD_AND_BLOCK_IF_PRESENT"},"tables":["comment","issue","issue_assignees","issue_content_history","issue_dependency","issue_label","issue_user","issue_watch","notification","project","project_board","project_issue","pull_auto_merge","pull_request","reaction","review","review_state","stopwatch","tracked_time"]},"packagesActions":{"actionsTarget":"DISABLED","legacyPayloadSecretTokenLogImported":false,"packageTarget":"DISABLED","physicalDisposition":"DROP_ONLY_AFTER_SCHEMA_RELATION_AND_PHYSICAL_CLOSURE_VERIFIER","planEvidence":"RELATIONSHIP_COUNTS_AND_SAFE_DECLARED_SIZES_ONLY"},"releasesLabelsProjects":{"archive":"SANITIZED_IMMUTABLE_ARCHIVE_ONLY","legacyRowsImported":false,"payloadExportedInPlan":false},"repositoryState":{"cachedAccessCountersStarsWatches":"DROP_RESET_RECOMPUTE","legacyHooksWebhooksKeysTokensSessionsCredentialsSecrets":"IMPORT_ZERO","topics":"VERIFIED_SEMANTIC_EMPTY_REBUILD_ZERO"},"units":{"actionsType10":"DISABLED_ZERO_TARGET_ROWS","disabledTypes":[6,7,9,10],"enabledCleanTypes":[1,2,3,4,5,8],"legacyConfigImported":false,"legacyRowsImported":false,"packagesType9":"DISABLED_ZERO_TARGET_ROWS"}},"predecessor":{"artifactSha256":"d6870b5583a2f329eadb4e6cda65fdf4d271532df5ffbf8bfb1403968a434672","dispositionFile":"deployment/gitea-incident-salvage/confirmed-disposition-v1.json","dispositionSha256":"0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c"},"remainingBlockers":["attachment-physical-verifier-pending","candidate-root-activation-hard-frozen","closure-report-review-pin-pending","collaboration-kept-user-mapping-verifier-pending","forensic-ref-archive-verifier-pending","issue-pr-metadata-sanitized-archive-verifier-pending","lfs-reachable-pointer-physical-verifier-pending","package-action-physical-closure-verifier-pending","reference-manifest-fsck-reachability-verifier-pending","repository-object-reconstruction-verifier-pending","target-unit-policy-acceptance-pending","unsupported-schema-catalog-verifier-pending"],"schemaVersion":"nodedc.gitea.incident-closure-disposition.v1","scope":{"deletedRepositories":2013,"deletedUsers":962,"keptRepositories":45,"keptUsers":10},"sourceEvidence":{"databaseSha256":"8db9e74a5641662a808d8252c5d6c9de43fe9efd8687634bfbff2d4361a13052","identityDecisionManifestSha256":"dc9528462624158eb44218d37cc7054d551ca2d7ded562592982aa3f34c9fc2a","referenceManifestSha256":"9cddaf0e4d4cf22dd264a6ae589ccc50d29e07f85c55e9d34b14627cecb8a311","semanticTopicsSha256":"df6e3612186234bfcf3c172ef4e0fff933baaa691a510f9780ebf9e21c8d4d05","snapshotUuid":"f5a3fe3a-93ea-bb4d-847f-6221a6bcbc9f","unsupportedRepositoryReportSha256":"4b2cecf88c62fc5c4a43419885e88a01c9f9aac03133afb19dae0a7caef106ac","unsupportedSchemaCatalogSha256":"b5e3b6776926c4f1627fafd882362ed0ef986bfc86fc6ac6507a43976531b6db"}}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,62 @@
name: nodedc-gitea
services:
gitea:
image: docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c
platform: linux/amd64
pull_policy: never
network_mode: none
user: "1000:1000"
entrypoint:
- /usr/local/bin/gitea
command:
- web
- --config
- /etc/gitea/app.ini
restart: "no"
stop_grace_period: 30s
init: true
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
pids_limit: 512
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment:
USER: git
HOME: /data/gitea
GITEA_WORK_DIR: /data/gitea
GITEA_CUSTOM: /data/gitea
volumes:
- type: bind
source: /volume1/docker/nodedc-gitea/data
target: /data
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-gitea/config
target: /etc/gitea
read_only: true
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-gitea/socket
target: /run/gitea
bind:
create_host_path: false
secrets:
- gitea_secret_key
- gitea_internal_token
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=256m
secrets:
gitea_secret_key:
file: /volume1/docker/nodedc-gitea/secrets/secret-key
gitea_internal_token:
file: /volume1/docker/nodedc-gitea/secrets/internal-token
@@ -0,0 +1,42 @@
{
"schemaVersion": "nodedc.gitea.fresh-install.v1",
"action": "fresh-install",
"component": "gitea",
"installRoot": "/volume1/docker/nodedc-gitea",
"compose": {
"file": "docker-compose.gitea.yml",
"project": "nodedc-gitea",
"service": "gitea",
"sha256": "25868a40996c405543b4627d06499b68f43556e607d839e969a400b0bc0ddadb"
},
"runtime": {
"image": "docker.gitea.com/gitea:1.27.1-rootless@sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148",
"platform": "linux/amd64",
"pullPolicy": "never",
"minimumComposeVersion": "2.20.1",
"transport": "unix:/run/gitea/gitea.sock",
"socketBind": "/volume1/docker/nodedc-gitea/socket:/run/gitea",
"ssh": "disabled-no-published-port",
"database": "fresh-sqlite-only",
"lfs": "disabled-pending-reviewed-restore-transition",
"networkMode": "none",
"logging": "bounded-json-file-10m-x3",
"stopGracePeriod": "30s"
},
"trust": {
"artifactSecrets": "forbidden",
"runtimeSecrets": "runner-managed-file-mounts",
"legacyRootAccess": "forbidden",
"legacyDatabaseImport": "forbidden",
"legacyRepositoryImport": "forbidden"
},
"reverseProxyPrerequisite": {
"managedOutsideArtifact": true,
"requiredDsmUpstream": "127.0.0.1:3000",
"requiredNginxBridge": "/usr/local/etc/nginx/conf.d/http.nodedc-gitea-uds.conf",
"requiredNginxBridgeSha256": "164f37a12a4f91e656cf20bd5b109978d16d723bdfde236653722aaf820780c9",
"requiredUnixUpstream": "/volume1/docker/nodedc-gitea/socket/gitea.sock",
"mustBeCompletedBeforeApply": true
},
"rollback": "stop-candidate-preserve-fresh-runtime-state-and-restore-source"
}
@@ -0,0 +1,128 @@
name: nodedc-gitea
services:
gitea:
image: docker.gitea.com/gitea:1.27.1-rootless@sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148
platform: linux/amd64
pull_policy: never
network_mode: none
user: "1000:1000"
restart: unless-stopped
stop_grace_period: 30s
init: true
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
pids_limit: 512
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment:
GITEA_WORK_DIR: /var/lib/gitea
GITEA__database__DB_TYPE: sqlite3
GITEA__database__PATH: /var/lib/gitea/data/gitea.db
GITEA__server__DOMAIN: git.dcserve.ru
GITEA__server__ROOT_URL: https://git.dcserve.ru/
GITEA__server__PROTOCOL: http+unix
GITEA__server__HTTP_ADDR: /run/gitea/gitea.sock
GITEA__server__UNIX_SOCKET_PERMISSION: "0666"
GITEA__server__LOCAL_ROOT_URL: http://unix/
GITEA__server__DISABLE_SSH: "true"
GITEA__server__START_SSH_SERVER: "false"
GITEA__server__SSH_CREATE_AUTHORIZED_KEYS_FILE: "false"
GITEA__server__LFS_START_SERVER: "false"
GITEA__server__LFS_ALLOW_PURE_SSH: "false"
GITEA__server__OFFLINE_MODE: "true"
GITEA__server__LANDING_PAGE: login
GITEA__security__INSTALL_LOCK: "true"
GITEA__security__SECRET_KEY_URI: file:/run/secrets/gitea_secret_key
GITEA__security__INTERNAL_TOKEN_URI: file:/run/secrets/gitea_internal_token
GITEA__security__REVERSE_PROXY_LIMIT: "1"
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 127.0.0.0/8,::1/128
GITEA__security__DISABLE_GIT_HOOKS: "true"
GITEA__security__DISABLE_WEBHOOKS: "true"
GITEA__security__IMPORT_LOCAL_PATHS: "false"
GITEA__security__ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET: "true"
GITEA__security__PASSWORD_HASH_ALGO: argon2
GITEA__security__MIN_PASSWORD_LENGTH: "16"
GITEA__security__PASSWORD_COMPLEXITY: lower,upper,digit,spec
GITEA__security__TWO_FACTOR_AUTH: enforced
GITEA__security__DISABLE_QUERY_AUTH_TOKEN: "true"
GITEA__security__ALLOWED_HOST_LIST: loopback
GITEA__service__DISABLE_REGISTRATION: "true"
GITEA__service__REQUIRE_SIGNIN_VIEW: "true"
GITEA__service__SHOW_REGISTRATION_BUTTON: "false"
GITEA__service__DEFAULT_KEEP_EMAIL_PRIVATE: "true"
GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION: "false"
GITEA__service__DEFAULT_USER_IS_RESTRICTED: "true"
GITEA__service__DEFAULT_USER_VISIBILITY: private
GITEA__service__ALLOWED_USER_VISIBILITY_MODES: private
GITEA__service__DEFAULT_ORG_VISIBILITY: private
GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION: "false"
GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION_API: "false"
GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "false"
GITEA__service__ENABLE_NOTIFY_MAIL: "false"
GITEA__service__ENABLE_BASIC_AUTHENTICATION: "false"
GITEA__admin__DISABLE_REGULAR_ORG_CREATION: "true"
GITEA__admin__USER_DISABLED_FEATURES: deletion,manage_ssh_keys,manage_gpg_keys,change_username
GITEA__repository__FORCE_PRIVATE: "true"
GITEA__repository__DEFAULT_PRIVATE: private
GITEA__repository__USER_MAX_CREATION_LIMIT: "0"
GITEA__repository__ORG_MAX_CREATION_LIMIT: "0"
GITEA__repository__ENABLE_PUSH_CREATE_USER: "false"
GITEA__repository__ENABLE_PUSH_CREATE_ORG: "false"
GITEA__repository__DISABLE_MIGRATIONS: "true"
GITEA__repository__ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES: "false"
GITEA__repository__ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES: "false"
GITEA__repository__DISABLE_HTTP_GIT: "false"
GITEA__repository.upload__ENABLED: "false"
GITEA__attachment__ENABLED: "false"
GITEA__actions__ENABLED: "false"
GITEA__packages__ENABLED: "false"
GITEA__oauth2__ENABLED: "false"
GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION: "false"
GITEA__openid__ENABLE_OPENID_SIGNIN: "false"
GITEA__openid__ENABLE_OPENID_SIGNUP: "false"
GITEA__federation__ENABLED: "false"
GITEA__mailer__ENABLED: "false"
GITEA__session__COOKIE_SECURE: "true"
GITEA__session__SAME_SITE: strict
GITEA__api__ENABLE_SWAGGER: "false"
GITEA__migrations__ALLOW_LOCALNETWORKS: "false"
GITEA__migrations__SKIP_TLS_VERIFY: "false"
GITEA__cors__ENABLED: "false"
GITEA__metrics__ENABLED: "false"
GITEA__cron.update_checker__ENABLED: "false"
GITEA__log__MODE: console
GITEA__log__LEVEL: Info
volumes:
- type: bind
source: /volume1/docker/nodedc-gitea/data
target: /var/lib/gitea
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-gitea/config
target: /etc/gitea
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-gitea/socket
target: /run/gitea
bind:
create_host_path: false
secrets:
- gitea_secret_key
- gitea_internal_token
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=256m
secrets:
gitea_secret_key:
file: /volume1/docker/nodedc-gitea/secrets/secret-key
gitea_internal_token:
file: /volume1/docker/nodedc-gitea/secrets/internal-token
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@ import importlib.machinery
import importlib.util
import inspect
import json
import stat
import tempfile
import unittest
from pathlib import Path
@@ -28,6 +29,909 @@ RUNNER = load_runner()
class DevicePlaneRegistryTest(unittest.TestCase):
def test_manager_v9_pins_v8_and_host_telemetry_workspace(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V9_PREDECESSOR_PATCH_ID,
"device-manager-release-v8-20260822-039",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V9_CONTROL_CORE_PREDECESSOR_PATCH_ID,
"device-control-core-release-v3-20260822-040",
)
boundaries = RUNNER.expected_device_plane_manager_release_v9_boundaries()
self.assertEqual(
boundaries["telemetryWorkspace"],
"mission-core-compute-module-parity-v1",
)
self.assertEqual(
boundaries["telemetryFreshness"],
"fifteen-seconds-missing-stale-not-unhealthy",
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V9_ENTRIES,
),
("device-manager",),
)
def test_manager_v10_pins_successful_v8_and_control_core_v4(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V10_PREDECESSOR_PATCH_ID,
"device-manager-release-v8-20260822-039",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V10_CONTROL_CORE_PREDECESSOR_PATCH_ID,
"device-control-core-release-v4-20260823-047",
)
boundaries = RUNNER.expected_device_plane_manager_release_v10_boundaries()
self.assertEqual(
boundaries["controlCorePredecessor"]["artifactSha256"],
"4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c",
)
self.assertEqual(
boundaries["telemetryWorkspace"],
"mission-core-compute-module-parity-v1",
)
self.assertTrue(
RUNNER.is_device_plane_manager_release_v10_slice(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES,
)
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES,
),
("device-manager",),
)
def test_manager_v11_pins_v10_and_mission_core_visual_parity(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V11_PREDECESSOR_PATCH_ID,
"device-manager-release-v10-20260823-048",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V11_PREDECESSOR_ARTIFACT_SHA256,
"e6b983a314db4f8c27d89062dfedf5ed0523cc30421170799d181a19e2d85d4c",
)
boundaries = RUNNER.expected_device_plane_manager_release_v11_boundaries()
self.assertEqual(
boundaries["missionCoreReference"],
"compute-modules-workspace-71c8b04",
)
self.assertEqual(
boundaries["telemetryWorkspace"],
"mission-core-compute-module-visual-parity-v2",
)
self.assertEqual(
boundaries["telemetrySurface"],
"borderless-soft-surface-v1",
)
self.assertEqual(
boundaries["telemetryScroll"],
"reset-on-workspace-transition-v1",
)
self.assertTrue(
RUNNER.is_device_plane_manager_release_v11_slice(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES,
)
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES,
),
("device-manager",),
)
def test_manager_v12_pins_v11_and_adaptive_telemetry_graphs(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V12_PREDECESSOR_PATCH_ID,
"device-manager-release-v11-20260823-049",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V12_PREDECESSOR_ARTIFACT_SHA256,
"c1e2056b50bfbb0d03d077461d0c27cc56cc52967c3f5620be14871c8a6d5cf0",
)
boundaries = RUNNER.expected_device_plane_manager_release_v12_boundaries()
self.assertEqual(
boundaries["telemetryWorkspace"],
"mission-core-compute-module-adaptive-window-v3",
)
self.assertEqual(
boundaries["telemetryGraphScale"],
"adaptive-observed-window-explicit-domain-v1",
)
self.assertEqual(
boundaries["telemetryNetworkMissingSemantics"],
"missing-counters-never-zero-v1",
)
self.assertTrue(
RUNNER.is_device_plane_manager_release_v12_slice(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES,
)
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES,
),
("device-manager",),
)
def test_manager_v13_pins_v12_and_host_inventory_accordion(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V13_PREDECESSOR_PATCH_ID,
"device-manager-release-v12-20260823-050",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V13_PREDECESSOR_ARTIFACT_SHA256,
"1a49839140e5f2e49763d78f24ee47d946e244bcfde15a9c38266e8bd14c0d49",
)
boundaries = RUNNER.expected_device_plane_manager_release_v13_boundaries()
self.assertEqual(
boundaries["hostInventoryComposition"],
"mission-core-compute-host-accordion-v2",
)
self.assertEqual(
boundaries["hostInventoryRow"],
"compact-centered-accordion-v1",
)
self.assertEqual(
boundaries["hostInventoryRelations"],
"host-scoped-endpoint-deployment-service-v1",
)
self.assertEqual(
boundaries["telemetryWorkspace"],
"mission-core-compute-module-adaptive-window-v3",
)
self.assertTrue(
RUNNER.is_device_plane_manager_release_v13_slice(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES,
)
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES,
),
("device-manager",),
)
def test_control_core_v3_registers_telemetry_contract_slice(self):
predecessor = {
"kind": "release",
"patchId": "device-control-core-release-v2-20260822-038",
"artifactSha256": (
"e2d062b82b022dba662522b5d6e192026ac964d78950d903295ca3cbbc95ab28"
),
}
descriptor = RUNNER.expected_device_plane_control_core_release_descriptor(
"device-control-core-release-v3-20260822-040",
predecessor,
schema_version="v3",
)
self.assertEqual(
descriptor["telemetryTransport"],
"edge-channel-host-telemetry-observed-v1",
)
self.assertEqual(
descriptor["telemetryStorage"],
"device-control-core-postgres-seven-day-retention",
)
self.assertTrue(
RUNNER.is_device_plane_control_core_release_v3_slice(
"device-plane",
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES,
)
)
def test_control_core_v4_pins_terminal_recovery_046(self):
predecessor = {
"kind": "migration-replay-checkpoint-recovery",
"patchId": (
"device-control-core-migration-replay-checkpoint-recovery-"
"20260822-046"
),
"artifactSha256": (
"46000c76977fb583fc7c9cf74ecf624efd8b404f7b8d0322e0270e7b8ac6e450"
),
}
descriptor = RUNNER.expected_device_plane_control_core_release_descriptor(
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_PATCH_ID,
predecessor,
schema_version="v4",
)
self.assertEqual(
descriptor["schemaVersion"],
"nodedc.device-plane.device-control-core-release.v4",
)
self.assertEqual(
descriptor["recoveryPredecessor"],
"terminal-applied-046-exact-source-runtime-database",
)
self.assertEqual(
descriptor["databaseSchemaOutcome"],
"migration-017-host-telemetry-table-present",
)
self.assertTrue(
RUNNER.is_device_plane_control_core_release_v4_slice(
"device-plane",
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES,
)
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES,
),
("device-control-core",),
)
def test_control_core_v4_health_contract_keeps_typed_commands(self):
checks = RUNNER.component_healthchecks(
"device-plane",
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES,
("device-control-core",),
)
self.assertEqual(
checks[0]["expected_json"]["commandTransport"],
"typed-service-ping-v1",
)
def test_control_core_v4_telemetry_database_acceptance(self):
result = mock.Mock(
returncode=0,
stdout="true\t19\t2\ttrue\t0\t0\n",
stderr="",
)
with (
mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
return_value=("a" * 64,),
),
mock.patch.object(
RUNNER.subprocess,
"run",
return_value=result,
),
):
evidence = (
RUNNER.collect_device_plane_control_core_host_telemetry_database_evidence()
)
self.assertTrue(evidence["tablePresent"])
self.assertEqual(evidence["columnCount"], 19)
self.assertEqual(evidence["indexCount"], 2)
self.assertEqual(evidence["expiredSampleCount"], 0)
def test_control_core_v4_telemetry_database_rejects_missing_index(self):
result = mock.Mock(
returncode=0,
stdout="true\t19\t1\ttrue\t0\t0\n",
stderr="",
)
with (
mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
return_value=("a" * 64,),
),
mock.patch.object(
RUNNER.subprocess,
"run",
return_value=result,
),
self.assertRaisesRegex(
RUNNER.DeployError,
"host telemetry database invariant mismatch",
),
):
RUNNER.collect_device_plane_control_core_host_telemetry_database_evidence()
def test_manager_v8_pins_production_v6_and_canonical_ontology_runtime(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V8_PREDECESSOR_PATCH_ID,
"device-manager-release-v6-20260822-035",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V8_PREDECESSOR_ARTIFACT_SHA256,
"193faabe930e2b3f212f8eb45288e39f850ec714528b28881095be654baf9a80",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V8_CONTROL_CORE_PREDECESSOR_PATCH_ID,
"device-control-core-release-v2-20260822-038",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V8_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256,
"e2d062b82b022dba662522b5d6e192026ac964d78950d903295ca3cbbc95ab28",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V8_COMPOSE_SHA256,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256,
)
boundaries = RUNNER.expected_device_plane_manager_release_v8_boundaries()
self.assertEqual(
boundaries["infrastructureHostProjection"],
"ontology-backed-host-runtime-v1",
)
self.assertEqual(
boundaries["ontologyFoundation"],
"ontology-core-device-foundation-20260822-001",
)
self.assertEqual(boundaries["ontologyCatalogHash"], "229c61c02a790906")
self.assertEqual(
boundaries["assetBinding"],
"temporal-device-asset-binding-v1",
)
self.assertEqual(
boundaries["healthEvidence"],
"ttl-observation-missing-not-unhealthy-v1",
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V8_ENTRIES,
),
("device-manager",),
)
def test_manager_v7_pins_v6_and_live_edge_host_projection(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_PATCH_ID,
"device-manager-release-v6-20260822-035",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_ARTIFACT_SHA256,
"193faabe930e2b3f212f8eb45288e39f850ec714528b28881095be654baf9a80",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_PATCH_ID,
"device-control-core-release-v2-20260822-036",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256,
"8708cc4b59fa0cd5e9c6e6a7b2654ba01ea60271549167aca2631f94000d3da3",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_COMPOSE_SHA256,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256,
)
boundaries = RUNNER.expected_device_plane_manager_release_v7_boundaries()
self.assertEqual(
boundaries["infrastructureHostProjection"],
"edge-registration-live-channel-v1",
)
self.assertEqual(
boundaries["ontologyStatus"],
"generic-host-domain-candidate-not-canonical",
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_ENTRIES,
),
("device-manager",),
)
def test_manager_v6_pins_034_and_canonical_favicon_boundary(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_PATCH_ID,
"device-manager-release-v5-20260822-034",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_ARTIFACT_SHA256,
"acc1d2ae2cda66861054826928c25d01a2428e688cc8132a9c381831bf29ab5a",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256,
)
boundaries = RUNNER.expected_device_plane_manager_release_v6_boundaries()
self.assertEqual(boundaries["faviconSet"], "nodedc-adaptive-v1")
self.assertEqual(
boundaries["overviewLayout"],
"mission-core-landing-stage-v1",
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
),
("device-manager",),
)
def test_manager_v5_pins_033_and_mission_core_overview_layout(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_PATCH_ID,
"device-manager-release-v4-20260822-033",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_ARTIFACT_SHA256,
"52ba322042f1e4f595bbfea99f8bb35630b15984e0da648dc55348bc9e5b2066",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256,
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
)
boundaries = RUNNER.expected_device_plane_manager_release_v5_boundaries()
self.assertEqual(
boundaries["overviewLayout"],
"mission-core-landing-stage-v1",
)
self.assertEqual(
RUNNER.component_services(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
),
("device-manager",),
)
def test_manager_v4_pins_032_and_persistent_white_boundary(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID,
"device-manager-release-v3-20260822-032",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256,
"6e0eb3a0a6f19ceab92d46832b93bffbcea21247dbdc2ea50625a51ff460e4ca",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
"e7dff0f5873ad4586bd55946d3db2bb86092a5e149e886d120adc041e056c256",
)
boundaries = RUNNER.expected_device_plane_manager_release_v4_boundaries()
self.assertEqual(boundaries["defaultAccentHex"], "#f5f5f5")
self.assertEqual(
boundaries["presentationDataHostPath"],
"/volume1/docker/nodedc-device-plane/data/device-manager",
)
self.assertEqual(
boundaries["presentationDataContainerPath"],
"/var/lib/nodedc-device-manager",
)
def test_manager_v3_targets_applied_control_core_recovery(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID,
"device-control-core-release-v2-20260821-030",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256,
"8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454",
)
self.assertEqual(
RUNNER.expected_device_plane_manager_release_v3_boundaries()[
"controlCorePredecessor"
],
{
"patchId": (
"device-control-core-release-v2-20260821-030"
),
"artifactSha256": (
"8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454"
),
},
)
def test_control_core_predecessor_health_is_phase_scoped(self):
with self.assertRaisesRegex(
RUNNER.DeployError,
"preflight phase is invalid",
):
RUNNER.validate_device_plane_control_core_release_predecessor(
Path("/not-used"),
preflight_phase="unknown",
)
plan_source = inspect.getsource(RUNNER.plan_artifact)
self.assertIn(
'validate_device_plane_control_core_release_predecessor(\n'
' payload_dir,\n'
' preflight_phase="plan",',
plan_source,
)
apply_source = inspect.getsource(RUNNER.apply_artifact)
self.assertIn(
'validate_device_plane_control_core_release_predecessor(\n'
' payload_dir,\n'
' preflight_phase="apply",',
apply_source,
)
def test_control_core_preflight_health_excludes_selected_target(self):
descriptor = {
"preservedServices": [
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target",
],
}
with mock.patch.object(
RUNNER,
"healthcheck_compose_service",
) as healthcheck:
services = (
RUNNER.validate_device_plane_control_core_preserved_runtime_health(
descriptor
)
)
self.assertEqual(
services,
(
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target",
),
)
self.assertEqual(
healthcheck.call_args_list,
[
mock.call("device-plane", "device-manager"),
mock.call("device-plane", "device-gateway"),
mock.call("device-plane", "device-postgres"),
mock.call("device-plane", "device-backhaul-target"),
],
)
self.assertNotIn(
mock.call("device-plane", "device-control-core"),
healthcheck.call_args_list,
)
def test_control_core_selected_predecessor_may_be_unhealthy(self):
inventory = {
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
"composeProject": "nodedc-device-plane",
"services": [{
"service": "device-control-core",
"containerId": "a" * 64,
"imageId": "sha256:" + "b" * 64,
"status": "running",
"running": True,
"health": "unhealthy",
"restartCount": 4,
}],
}
with mock.patch.object(
RUNNER,
"device_plane_runtime_inventory",
return_value=inventory,
):
selected = (
RUNNER.validate_device_plane_control_core_selected_predecessor_runtime()
)
self.assertEqual(selected["health"], "unhealthy")
def test_control_core_selected_predecessor_may_be_restarting(self):
inventory = {
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
"composeProject": "nodedc-device-plane",
"services": [{
"service": "device-control-core",
"containerId": "a" * 64,
"imageId": "sha256:" + "b" * 64,
"status": "restarting",
"running": True,
"health": "starting",
"restartCount": 5,
}],
}
with mock.patch.object(
RUNNER,
"device_plane_runtime_inventory",
return_value=inventory,
):
selected = (
RUNNER.validate_device_plane_control_core_selected_predecessor_runtime()
)
self.assertEqual(selected["status"], "restarting")
self.assertEqual(selected["health"], "starting")
def test_control_core_rollback_accepts_restored_unhealthy_boundary(self):
service_names = (
"device-control-core",
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target",
)
inventory = {
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
"composeProject": "nodedc-device-plane",
"services": [
{
"service": service,
"containerId": chr(97 + index) * 64,
"imageId": "sha256:" + str(index + 1) * 64,
"status": "running",
"running": True,
"health": "unhealthy" if index == 0 else "healthy",
"restartCount": index,
}
for index, service in enumerate(service_names)
],
}
restored_id = "f" * 64
restored = {
"Id": restored_id,
"State": {
"Status": "running",
"Running": True,
"Health": {"Status": "unhealthy"},
},
}
preserved_inventory = {
**inventory,
"services": inventory["services"][1:],
}
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
) as preserved_health,
mock.patch.object(
RUNNER,
"device_plane_runtime_inventory",
return_value=preserved_inventory,
),
mock.patch.object(
RUNNER,
"compose_service_container_id",
return_value=restored_id,
),
mock.patch.object(
RUNNER,
"inspect_device_plane_container",
return_value=restored,
),
):
accepted = (
RUNNER.accept_device_plane_control_core_rollback_runtime(
inventory
)
)
self.assertEqual(accepted["health"], "unhealthy")
self.assertEqual(accepted["status"], "running")
self.assertEqual(accepted["predecessorHealth"], "unhealthy")
self.assertEqual(
preserved_health.call_args_list,
[
mock.call("device-plane", "device-manager"),
mock.call("device-plane", "device-gateway"),
mock.call("device-plane", "device-postgres"),
mock.call("device-plane", "device-backhaul-target"),
],
)
def test_control_core_post_apply_health_includes_backhaul(self):
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
) as healthcheck,
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(),
),
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
),
):
RUNNER.run_healthchecks(
"device-plane",
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
("device-control-core",),
)
self.assertEqual(
healthcheck.call_args_list,
[
mock.call("device-plane", "device-control-core"),
mock.call("device-plane", "device-manager"),
mock.call("device-plane", "device-gateway"),
mock.call("device-plane", "device-postgres"),
mock.call("device-plane", "device-backhaul-target"),
],
)
def test_manager_v3_post_apply_uses_private_egress_core_boundary(self):
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
),
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(),
),
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
) as runtime_acceptance,
):
RUNNER.run_healthchecks(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
("device-manager",),
)
runtime_acceptance.assert_called_once_with(
require_edge_channel=True,
core_network_mode="private-egress",
)
def test_manager_v4_post_apply_requires_persistent_data(self):
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
),
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(),
),
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
) as runtime_acceptance,
):
RUNNER.run_healthchecks(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES,
("device-manager",),
)
runtime_acceptance.assert_called_once_with(
require_edge_channel=True,
core_network_mode="private-egress",
require_persistent_data=True,
)
def test_manager_v5_post_apply_preserves_persistent_data_gate(self):
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
),
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(),
),
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
) as runtime_acceptance,
):
RUNNER.run_healthchecks(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
("device-manager",),
)
runtime_acceptance.assert_called_once_with(
require_edge_channel=True,
core_network_mode="private-egress",
require_persistent_data=True,
)
def test_manager_v6_post_apply_preserves_persistent_data_gate(self):
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
),
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(),
),
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
) as runtime_acceptance,
):
RUNNER.run_healthchecks(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
("device-manager",),
)
runtime_acceptance.assert_called_once_with(
require_edge_channel=True,
core_network_mode="private-egress",
require_persistent_data=True,
)
def test_manager_v4_prepare_owns_new_parent_and_managed_data_directory(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-manager-v4-data-",
) as directory:
data_dir = Path(directory) / "data" / "device-manager"
with (
mock.patch.object(
RUNNER,
"DEVICE_PLANE_MANAGER_DATA_DIR",
data_dir,
),
mock.patch.object(RUNNER.os, "chown") as chown,
mock.patch.object(
RUNNER,
"validate_device_plane_manager_persistent_data_metadata",
return_value="uid-1000-gid-1000-mode-0750",
) as validate,
):
result = RUNNER.ensure_device_plane_manager_persistent_data()
self.assertEqual(result, "uid-1000-gid-1000-mode-0750")
self.assertEqual(
chown.call_args_list,
[
mock.call(data_dir.parent, 0, 0),
mock.call(data_dir, 1000, 1000),
],
)
validate.assert_called_once_with()
def test_manager_v4_prepare_preserves_existing_safe_parent_metadata(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-manager-v4-parent-",
) as directory:
data_parent = Path(directory) / "data"
data_parent.mkdir(mode=0o700)
data_dir = data_parent / "device-manager"
with (
mock.patch.object(
RUNNER,
"DEVICE_PLANE_MANAGER_DATA_DIR",
data_dir,
),
mock.patch.object(RUNNER.os, "chown") as chown,
mock.patch.object(
RUNNER,
"validate_device_plane_manager_persistent_data_metadata",
return_value="uid-1000-gid-1000-mode-0750",
),
):
RUNNER.ensure_device_plane_manager_persistent_data()
self.assertEqual(stat.S_IMODE(data_parent.stat().st_mode), 0o700)
chown.assert_called_once_with(data_dir, 1000, 1000)
def test_manager_v4_prepare_rejects_symlink_parent_before_child_creation(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-manager-v4-symlink-",
) as directory:
root = Path(directory)
outside = root / "outside"
outside.mkdir()
data_parent = root / "data"
data_parent.symlink_to(outside, target_is_directory=True)
with mock.patch.object(
RUNNER,
"DEVICE_PLANE_MANAGER_DATA_DIR",
data_parent / "device-manager",
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"persistent data parent is unsafe",
):
RUNNER.ensure_device_plane_manager_persistent_data()
self.assertFalse((outside / "device-manager").exists())
def test_manager_predecessor_health_is_phase_scoped(self):
with self.assertRaisesRegex(
RUNNER.DeployError,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
import hashlib
import importlib.machinery
import importlib.util
import io
import json
import stat
from types import SimpleNamespace
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parent
def load(name, path):
loader = importlib.machinery.SourceFileLoader(name, str(path))
spec = importlib.util.spec_from_loader(name, loader)
module = importlib.util.module_from_spec(spec); loader.exec_module(module)
return module
R = load("map_access_runner", ROOT / "nodedc-deploy")
B = load("map_access_builder", ROOT / "build-mission-core-map-access-artifact.py")
class MapAccessTests(unittest.TestCase):
def test_scope_never_selects_docker_or_accepts_host_policy_payload(self):
name = "mission-core-map-access"
self.assertEqual(R.component_services(name, ["access.json"]), ())
self.assertEqual(R.component_builds(name, ["access.json"]), ())
self.assertTrue(R.component_artifact_only(name))
self.assertTrue(R.allowed_payload_path(name, "access.json"))
for path in ("sshd_config", "../sshd_config", "script.py", "docker-compose.yml", ".env", "secrets/key"):
with self.assertRaises(R.DeployError): R.allowed_payload_path(name, path)
def test_deterministic_data_bundle_and_fail_closed_descriptor(self):
raw = B.build("map-access-test-001")
self.assertEqual(raw, B.build("map-access-test-001"))
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact = root / "candidate.tgz"
artifact.write_bytes(raw)
with tarfile.open(fileobj=io.BytesIO(raw)) as archive:
self.assertEqual(archive.getnames(), ["manifest.env", "files.txt", "payload/access.json"])
unpacked = root / "unpacked"
unpacked.mkdir()
manifest, entries, payload = R.load_artifact(artifact, unpacked)
self.assertEqual(manifest["component"], "mission-core-map-access")
self.assertEqual(R.read_map_access_descriptor(payload, entries), "enabled")
for value in ({"schemaVersion":"nodedc.mission-core-map-access.v1","state":"enabled","user":"root"}, {"schemaVersion":"unknown","state":"enabled"}):
(payload/"access.json").write_text(json.dumps(value))
with self.assertRaises(R.DeployError): R.read_map_access_descriptor(payload, entries)
def test_exact_predecessor_idempotence_and_explicit_disable(self):
base = b"AllowTcpForwarding no\n"
with patch.object(R, "MAP_ACCESS_BASE_SHA", hashlib.sha256(base).hexdigest()):
enabled = R.map_access_policy(base, "enabled")
self.assertEqual(enabled, base + R.MAP_ACCESS_APPEND)
self.assertEqual(R.map_access_policy(enabled, "enabled"), enabled)
self.assertEqual(R.map_access_policy(enabled, "disabled"), base)
for drift in (base+b"# external drift\n", enabled+b"# external drift\n", enabled+R.MAP_ACCESS_APPEND):
with self.assertRaises(R.DeployError): R.map_access_policy(drift, "enabled")
def test_effective_policy_may_only_change_exact_user_and_destination(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(R, "TMP_DIR", Path(tmp)), patch.object(R.subprocess,"run") as run:
run.return_value.returncode=0
candidate=Path(tmp)/"candidate";candidate.write_bytes(R.MAP_ACCESS_APPEND)
baseline={"allowtcpforwarding":"no", "permitopen":"any", "passwordauthentication":"yes"}
def effective(config,user,address):
value=dict(baseline)
if str(config)==str(candidate) and user=="dctouch":value.update(allowtcpforwarding="local",permitopen="127.0.0.1:18103")
return value
with patch.object(R,"map_access_effective",side_effect=effective):R.map_access_validate_candidate(b"baseline",candidate)
def broad(config,user,address):
value=effective(config,user,address)
if str(config)==str(candidate):value["permitopen"]="any"
return value
with patch.object(R,"map_access_effective",side_effect=broad), self.assertRaises(R.DeployError):R.map_access_validate_candidate(b"baseline",candidate)
def test_reload_failure_is_routed_to_domain_rollback(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(R,"preflight_map_access",return_value=(b"old",b"new")), patch.object(R,"map_access_replace") as replace, patch.object(R,"map_access_reload",side_effect=R.DeployError("reload")):
with self.assertRaises(R.DeployError):R.apply_map_access(Path(tmp),["access.json"])
replace.assert_called_once_with(b"new")
def test_rollback_restores_exact_policy_and_descriptor(self):
base=b"AllowTcpForwarding no\n"
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp); backup=root/"backup"; backup.mkdir()
(backup/"sshd-config-before").write_bytes(base)
calls=[]
metadata=SimpleNamespace(st_mode=stat.S_IFREG|0o600,st_uid=0)
with patch.object(R,"MAP_ACCESS_BASE_SHA",hashlib.sha256(base).hexdigest()), patch.object(Path,"lstat",return_value=metadata), patch.object(R,"map_access_read_config",side_effect=[base+R.MAP_ACCESS_APPEND,base]), patch.object(R,"map_access_validate_candidate"), patch.object(R,"map_access_replace",side_effect=lambda value:calls.append(("replace",value))), patch.object(R,"map_access_reload",side_effect=lambda:calls.append(("reload",))), patch.object(R,"restore_overlay_source",side_effect=lambda *args:calls.append(("restore",))):
R.rollback_map_access(root,backup,["access.json"],"stamp")
self.assertEqual(calls,[("replace",base),("reload",),("restore",)])
if __name__ == "__main__": unittest.main()
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
PLATFORM_ROOT = SCRIPT_DIR.parent.parent
BUILDER_PATH = SCRIPT_DIR / "build-tasker-attachment-formats-artifact.mjs"
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
RELEASE = "20260829-001"
SOURCE_COMMIT = "f9308539bfe71e4e2359c47ed5c230fa1fb386c9"
EXPECTED_FILES = {
"plane-src/apps/api/plane/settings/common.py",
"plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx",
"plane-src/apps/web/core/components/issues/peek-overview/view.tsx",
"plane-src/apps/web/styles/globals.css",
"plane-src/packages/services/src/file/helper.ts",
}
def load_runner():
loader = importlib.machinery.SourceFileLoader("nodedc_tasker_attachment_formats_runner", str(RUNNER_PATH))
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
RUNNER = load_runner()
class TaskerAttachmentFormatsArtifactTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temporary = tempfile.TemporaryDirectory(prefix="nodedc-tasker-attachment-formats-")
cls.root = Path(cls.temporary.name)
cls.builds = []
for index in range(2):
output = cls.root / f"build-{index}"
output.mkdir()
env = os.environ.copy()
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(output)
result = subprocess.run(
["node", str(BUILDER_PATH), RELEASE],
cwd=PLATFORM_ROOT,
env=env,
check=True,
capture_output=True,
text=True,
)
cls.builds.append(json.loads(result.stdout))
@classmethod
def tearDownClass(cls):
cls.temporary.cleanup()
def test_artifact_is_reproducible_and_runner_accepted(self):
first = self.builds[0]
second = self.builds[1]
first_path = Path(first["artifact"])
second_path = Path(second["artifact"])
first_bytes = first_path.read_bytes()
self.assertEqual(first_bytes, second_path.read_bytes())
self.assertEqual(first_bytes[4:8], bytes(4))
self.assertEqual(first["sha256"], hashlib.sha256(first_bytes).hexdigest())
self.assertEqual(first["sourceCommit"], SOURCE_COMMIT)
self.assertEqual(first["patchId"], f"tasker-attachment-formats-{RELEASE}")
self.assertEqual(set(first["files"]), EXPECTED_FILES)
with tempfile.TemporaryDirectory() as directory:
manifest, entries, payload = RUNNER.load_artifact(first_path, Path(directory))
self.assertEqual(manifest, {
"id": first["patchId"],
"component": "tasker",
"type": "app-overlay",
})
self.assertEqual(entries, first["files"])
for relative_path in entries:
self.assertTrue((payload / relative_path).is_file())
def test_runner_selects_only_registered_tasker_scope(self):
entries = self.builds[0]["files"]
self.assertEqual(RUNNER.component_services("tasker", entries), ("api", "worker", "beat-worker", "web"))
self.assertEqual(len(RUNNER.component_builds("tasker", entries)), 2)
def test_builder_rejects_an_unexpected_release(self):
env = os.environ.copy()
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(self.root / "unexpected-release")
result = subprocess.run(
["node", str(BUILDER_PATH), "20260829-002"],
cwd=PLATFORM_ROOT,
env=env,
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("usage: build-tasker-attachment-formats-artifact.mjs", result.stderr)
if __name__ == "__main__":
unittest.main(verbosity=2)
+5
View File
@@ -167,6 +167,11 @@ Current package:
- `seo` - NDC SEO mod domain ontology for site scans, project ontology instances, scope contracts, semantic analysis, market evidence, optimization planning, validation, changesets, and future app-owned SEO assistant actions.
- `map` - provider-neutral NDC Module Studio map ontology for spatial subjects, layers, routes, zones, shared labels, visibility rules, selection, and replaceable renderer adapters.
- `gelios` - provider-neutral Gelios fleet and telemetry ontology, bound to map.moving_object, map.zone and map.place_target without exposing provider credentials or renderer objects.
- `party` - provider-neutral people, organizations, accounts, access subjects, contacts and temporal operational roles.
- `asset` - stable business assets, external references, assignments and lifecycle evidence independent of tracker or provider identity.
- `device` - Device Core direct onboarding, adapter, enrollment, temporal Asset binding, Edge route, session, configuration, typed command and audit ontology.
- `infrastructure` - provider-neutral Host, Endpoint, Deployment, Service Instance and privileged management-session topology for VPS and Edge infrastructure.
- `observation` - time-qualified property, source, freshness, health and position evidence for Assets, Devices and infrastructure.
The package loader merges domain packages into the base catalog before validation. Domain packages extend core meanings; they do not own app data or execute mutations.
@@ -0,0 +1,13 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"aliases": [
{ "alias": "business asset", "canonicalId": "asset.asset" },
{ "alias": "бизнес-актив", "canonicalId": "asset.asset" },
{ "alias": "трайк как актив", "canonicalId": "asset.asset" },
{ "alias": "тип актива", "canonicalId": "asset.asset_type" },
{ "alias": "внешняя ссылка актива", "canonicalId": "asset.external_reference" },
{ "alias": "назначение актива", "canonicalId": "asset.assignment" },
{ "alias": "событие жизненного цикла актива", "canonicalId": "asset.lifecycle_event" }
]
}
@@ -0,0 +1,11 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"entities": [
{ "id": "asset.asset", "name": "Asset", "surface": "domain", "status": ["product-required"], "authority": "Owning business domain", "summary": "Stable business or operational object such as a trike, vehicle, machine or facility whose identity survives device, provider and renderer changes." },
{ "id": "asset.asset_type", "name": "Asset Type", "surface": "domain", "status": ["product-required"], "authority": "Owning business domain + Ontology Core", "summary": "Versioned semantic classification of an asset without forcing a rigid inheritance hierarchy." },
{ "id": "asset.external_reference", "name": "Asset External Reference", "surface": "domain", "status": ["product-required"], "authority": "Owning business domain", "summary": "Namespaced reference linking an asset to a source-system record without replacing the NODE.DC asset identity." },
{ "id": "asset.assignment", "name": "Asset Assignment", "surface": "domain", "status": ["product-required"], "authority": "Owning operations domain", "summary": "Time-qualified relationship connecting an asset to a party, role, project or other operational context with provenance and validity interval." },
{ "id": "asset.lifecycle_event", "name": "Asset Lifecycle Event", "surface": "domain", "status": ["product-required"], "authority": "Owning business domain", "summary": "Immutable evidence of an asset lifecycle transition such as commissioning, activation, suspension, retirement or transfer." }
]
}
@@ -0,0 +1,18 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"sourceRoots": [
{ "surface": "device-core", "path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_DEVICE_CORE", "mode": "Read-only device, project, binding, direct lifecycle and restricted-identifier source inspection" },
{ "surface": "foundry", "path": "foundry://application/1c7dcdbb-6e50-4272-b1bc-aa5ece77ae99", "mode": "Robot2B Data Product composition and moving-object projection inspection" },
{ "surface": "ops-decision", "path": "DCPLATFORM-70", "mode": "Accepted direct B2, trike Asset and temporal attachment decision" }
],
"ledgers": [
{ "id": "ledger.asset_domain_v0", "path": "docs/PARTY_ASSET_DOMAIN_ONTOLOGY.md", "entityIds": ["asset.asset", "asset.asset_type", "asset.external_reference", "asset.assignment", "asset.lifecycle_event"] }
],
"baselineDocs": ["docs/PARTY_ASSET_DOMAIN_ONTOLOGY.md"],
"restrictions": [
"Do not use IMEI, provider unit ID, current tracker or renderer ID as the Asset identity.",
"Do not hide reassignment history by mutating one current owner, driver or device field.",
"Do not publish an Asset to a map without a declared spatial observation and Data Product semantic binding."
]
}
@@ -0,0 +1,38 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"rules": [
{
"id": "guardrail.asset.identity_survives_device_changes",
"severity": "error",
"summary": "Asset identity must not be derived from IMEI, tracker, provider unit, current driver, project or renderer object; those are separate graph subjects and assignments.",
"entityIds": ["asset.asset", "device.tracking_device", "device.restricted_identifier", "gelios.unit", "party.driver_role", "map.moving_object"]
},
{
"id": "guardrail.asset.assignments_are_temporal",
"severity": "error",
"summary": "Asset-to-party, driver, project and device composition requires explicit provenance and valid_from/valid_to semantics; overwriting the current label is not history.",
"entityIds": ["asset.asset", "asset.assignment", "party.driver_role", "device.asset_binding"]
},
{
"id": "guardrail.asset.map_projection_requires_spatial_evidence",
"severity": "error",
"summary": "Foundry may render an asset as a moving object only through a declared Data Product binding backed by a time-qualified spatial observation.",
"entityIds": ["asset.asset", "map.moving_object", "observation.position_observation"]
},
{
"id": "guardrail.asset.ontology_not_asset_store",
"severity": "error",
"summary": "Ontology Core defines Asset meaning and relations; it must not store runtime asset rows, assignments, live positions or lifecycle logs.",
"entityIds": ["asset.asset", "asset.assignment", "asset.lifecycle_event"]
}
],
"blockedConflations": [
["asset.asset", "device.device"],
["asset.asset", "device.tracking_device"],
["asset.asset", "device.restricted_identifier"],
["asset.asset", "gelios.unit"],
["asset.asset", "map.moving_object"],
["asset.assignment", "asset.lifecycle_event"]
]
}
@@ -0,0 +1,7 @@
{
"id": "asset",
"version": "0.1.0",
"updatedAt": "2026-08-22",
"status": "product-required/source-evidenced",
"summary": "Stable business assets, classifications, external references, temporal assignments and lifecycle evidence independent of a tracker, provider or renderer."
}
@@ -0,0 +1,12 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"relations": [
{ "id": "asset.asset.classified_by_type", "from": ["asset.asset"], "to": ["asset.asset_type"], "status": "product-required", "summary": "An asset uses a versioned semantic type while remaining a stable shallow graph subject." },
{ "id": "asset.asset.has_external_reference", "from": ["asset.asset"], "to": ["asset.external_reference"], "status": "product-required", "summary": "An asset may have multiple namespaced provider or source-system references." },
{ "id": "asset.asset.has_assignment", "from": ["asset.asset"], "to": ["asset.assignment"], "status": "product-required", "summary": "Operational composition is represented by explicit assignments with provenance and validity intervals." },
{ "id": "asset.assignment.assigns_party", "from": ["asset.assignment"], "to": ["party.party", "party.driver_role"], "status": "product-required", "summary": "An assignment connects the asset to a party or operational role for a bounded interval." },
{ "id": "asset.asset.has_lifecycle_event", "from": ["asset.asset"], "to": ["asset.lifecycle_event"], "status": "product-required", "summary": "Lifecycle transitions are preserved as evidence rather than overwritten status labels." },
{ "id": "asset.asset.is_map_moving_object", "from": ["asset.asset"], "to": ["map.moving_object"], "status": "product-required", "summary": "An asset with spatial evidence can be projected into Foundry through the provider-neutral moving-object contract without adopting renderer identity." }
]
}
@@ -0,0 +1,20 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"aliases": [
{ "alias": "управляемое устройство Device Core", "canonicalId": "device.device" },
{ "alias": "трекер Device Core", "canonicalId": "device.tracking_device" },
{ "alias": "B2 tracker", "canonicalId": "device.tracking_device" },
{ "alias": "привязка трекера к активу", "canonicalId": "device.asset_binding" },
{ "alias": "IMEI устройства", "canonicalId": "device.restricted_identifier" },
{ "alias": "пакет адаптера устройства", "canonicalId": "device.adapter_package" },
{ "alias": "профиль модели устройства", "canonicalId": "device.model_profile" },
{ "alias": "намерение регистрации устройства", "canonicalId": "device.enrollment_intent" },
{ "alias": "регистрация Edge Device Core", "canonicalId": "device.edge_registration" },
{ "alias": "маршрут устройства Device Core", "canonicalId": "device.route" },
{ "alias": "сессия устройства Device Core", "canonicalId": "device.session" },
{ "alias": "ревизия конфигурации устройства", "canonicalId": "device.configuration_revision" },
{ "alias": "командное намерение устройства", "canonicalId": "device.command_intent" },
{ "alias": "аудит Device Core", "canonicalId": "device.audit_event" }
]
}
@@ -0,0 +1,26 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"entities": [
{ "id": "device.project", "name": "Device Project", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Owner-scoped operational boundary that grants access to device, asset and infrastructure composition without becoming the identity of those subjects." },
{ "id": "device.collection", "name": "Device Collection", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Logical project-local grouping of devices or assets; grouping is not ownership, topology or attachment." },
{ "id": "device.device", "name": "Device", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Stable physical or logical device identity managed by Device Core independently from an attached asset, identifier, adapter, provider or route." },
{ "id": "device.tracking_device", "name": "Tracking Device", "surface": "device-core", "status": ["product-required", "source-evidenced"], "authority": "Device Core", "summary": "Device role that produces time-qualified tracking observations for an asset. A B2 is onboarded as a tracking device while remaining independent from the trike it tracks." },
{ "id": "device.asset_binding", "name": "Device Asset Binding", "surface": "device-core", "status": ["product-required", "source-evidenced"], "authority": "Device Core", "summary": "Explicit time-qualified attachment between one device and one asset with validity interval, provenance and lifecycle state." },
{ "id": "device.restricted_identifier", "name": "Restricted Device Identifier", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Namespaced hardware or integration identifier such as IMEI, serial or provider device id protected by field policy and never used as Asset identity." },
{ "id": "device.credential_reference", "name": "Device Credential Reference", "surface": "device-core", "status": ["source-confirmed"], "authority": "Device Core secret boundary", "summary": "Opaque reference to a credential owned by a secret authority; the credential value never enters Ontology Core, Data Products or browser state." },
{ "id": "device.adapter_package", "name": "Device Adapter Package", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Stable adapter family describing how a class of devices is decoded and controlled without making a vendor model canonical ontology." },
{ "id": "device.adapter_version", "name": "Device Adapter Version", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Immutable executable adapter release selected by a model profile or route." },
{ "id": "device.model_profile", "name": "Device Model Profile", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Versioned device model and capability profile binding protocol expectations to an adapter version." },
{ "id": "device.enrollment_intent", "name": "Device Enrollment Intent", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Bounded, auditable request to register or claim a discovered/direct device into an owner and project scope." },
{ "id": "device.discovery", "name": "Device Discovery", "surface": "device-core", "status": ["source-confirmed"], "authority": "Device Core", "summary": "Untrusted candidate evidence observed before enrollment and ownership acceptance; it is not yet a managed device." },
{ "id": "device.edge_registration", "name": "Device Edge Registration", "surface": "device-plane", "status": ["source-confirmed", "product-required"], "authority": "Device Control Core", "summary": "Core-approved Edge identity and endpoint registration participating in the core-initiated pinned-mTLS channel." },
{ "id": "device.route", "name": "Device Route", "surface": "device-plane", "status": ["source-confirmed", "product-required"], "authority": "Device Control Core", "summary": "Approved device traffic route resolving a device/model profile to a registered Edge and adapter contract." },
{ "id": "device.session", "name": "Device Session", "surface": "device-plane", "status": ["source-confirmed", "product-required"], "authority": "Device Control Core", "summary": "Bounded runtime communication session for one device over one approved route." },
{ "id": "device.configuration_revision", "name": "Device Configuration Revision", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Immutable desired configuration revision for a device or model profile." },
{ "id": "device.configuration_state", "name": "Device Configuration State", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Reconciled desired/observed configuration state with revision, evidence and time; it is not the configuration secret payload." },
{ "id": "device.command_intent", "name": "Device Command Intent", "surface": "device-command", "status": ["source-confirmed", "product-required"], "authority": "Device Control Core", "summary": "Explicit governed request for one typed command against one authorized device target, separate from transport and result." },
{ "id": "device.command_event", "name": "Device Command Event", "surface": "device-command", "status": ["source-confirmed", "product-required"], "authority": "Device Control Core", "summary": "Immutable lifecycle evidence for command acceptance, dispatch, acknowledgement, expiry, rejection or failure." },
{ "id": "device.audit_event", "name": "Device Audit Event", "surface": "device-core", "status": ["source-confirmed", "product-required"], "authority": "Device Core", "summary": "Immutable evidence of a governed Device Core action, actor, target, decision and result." }
]
}
@@ -0,0 +1,20 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"sourceRoots": [
{ "surface": "device-core", "path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_DEVICE_CORE", "mode": "Read-only inspection of migrations 001, 002, 004, 006, 008, 010, 013 and 015 plus Device Plane contracts" },
{ "surface": "device-edge-adr", "path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_DEVICE_CORE/docs/ADR_0001_CORE_INITIATED_EDGE_CHANNEL.md", "mode": "Core-initiated pinned-mTLS Edge identity, trust and command-boundary evidence" },
{ "surface": "ops-decision", "path": "DCPLATFORM-70", "mode": "Accepted direct B2 onboarding and temporal trike composition" },
{ "surface": "ops-decision", "path": "DCPLATFORM-76", "mode": "Device Core release and architecture milestone" }
],
"ledgers": [
{ "id": "ledger.device_domain_v0", "path": "docs/DEVICE_DOMAIN_ONTOLOGY.md", "entityIds": ["device.project", "device.collection", "device.device", "device.tracking_device", "device.asset_binding", "device.restricted_identifier", "device.adapter_package", "device.adapter_version", "device.model_profile", "device.enrollment_intent", "device.discovery", "device.edge_registration", "device.route", "device.session", "device.configuration_revision", "device.configuration_state", "device.command_intent", "device.command_event", "device.audit_event"] }
],
"baselineDocs": ["docs/DEVICE_DOMAIN_ONTOLOGY.md"],
"restrictions": [
"Do not copy device credentials, private keys, raw command payloads, runtime sessions or observations into Ontology Core.",
"Do not make B2, Gelios or another vendor/provider the canonical root of Device Core ontology.",
"Do not accept direct traffic before enrollment, model-profile, route and Edge-registration checks.",
"Do not expose arbitrary remote shell or untyped command transport as a Device Core command."
]
}
@@ -0,0 +1,58 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"rules": [
{
"id": "guardrail.device.direct_onboarding_is_provider_neutral",
"severity": "error",
"summary": "New direct B2 onboarding uses Device Core device, model profile, adapter, enrollment and asset-binding contracts. Gelios or another provider may supply legacy evidence but may not become the product identity model.",
"entityIds": ["device.device", "device.tracking_device", "device.model_profile", "device.enrollment_intent", "device.asset_binding", "gelios.tracker_device"]
},
{
"id": "guardrail.device.identifiers_are_restricted_not_identity",
"severity": "error",
"summary": "IMEI, serial, integration device id and credential references are restricted device attributes. They must not become Asset identity, public Data Product keys or browser-readable secrets.",
"entityIds": ["device.device", "device.restricted_identifier", "device.credential_reference", "asset.asset"]
},
{
"id": "guardrail.device.asset_attachment_is_temporal",
"severity": "error",
"summary": "A device-to-asset attachment requires a binding record with valid_from, valid_to, lifecycle state and provenance. Replacing B2 must not replace the trike Asset.",
"entityIds": ["device.tracking_device", "device.asset_binding", "asset.asset"]
},
{
"id": "guardrail.device.discovery_cannot_claim_ownership",
"severity": "error",
"summary": "Discovery is untrusted evidence. Only an authorized enrollment transition may create or claim a managed device and assign owner/project scope.",
"entityIds": ["device.discovery", "device.enrollment_intent", "device.device", "device.project"]
},
{
"id": "guardrail.device.commands_are_typed_red_domain",
"severity": "error",
"summary": "Device commands are typed, explicitly authorized intents with bounded TTL and audit. Arbitrary shell, firmware, reboot, memory wipe or provider command text is not exposed through the safe service-ping contract.",
"entityIds": ["device.command_intent", "device.command_event", "device.route", "device.audit_event"]
},
{
"id": "guardrail.device.edge_identity_is_not_host_identity",
"severity": "error",
"summary": "An Edge registration is an application identity realized by a service instance. It must not be conflated with a VPS host, network endpoint or deployment artifact.",
"entityIds": ["device.edge_registration", "infrastructure.host", "infrastructure.endpoint", "infrastructure.deployment", "infrastructure.service_instance"]
},
{
"id": "guardrail.device.ontology_does_not_store_runtime",
"severity": "error",
"summary": "Ontology Core describes Device meanings and relations only; Device Core owns device rows, sessions, routes, configuration, command events, observations, audit and secret references.",
"entityIds": ["device.device", "device.session", "device.route", "device.configuration_state", "device.command_event", "device.audit_event"]
}
],
"blockedConflations": [
["device.device", "device.restricted_identifier"],
["device.device", "device.model_profile"],
["device.tracking_device", "asset.asset"],
["device.discovery", "device.device"],
["device.edge_registration", "infrastructure.host"],
["device.route", "device.session"],
["device.configuration_revision", "device.configuration_state"],
["device.command_intent", "device.command_event"]
]
}
@@ -0,0 +1,7 @@
{
"id": "device",
"version": "0.1.0",
"updatedAt": "2026-08-22",
"status": "product-required/source-evidenced",
"summary": "Device Core domain ontology for direct device onboarding, asset composition, adapters, enrollment, Edge routing, sessions, configuration, governed commands and audit."
}
@@ -0,0 +1,31 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"relations": [
{ "id": "device.project.contains_collection", "from": ["device.project"], "to": ["device.collection"], "status": "source-confirmed", "summary": "A project contains logical collections without changing device or asset identity." },
{ "id": "device.project.scopes_subject", "from": ["device.project"], "to": ["asset.asset", "device.device", "device.edge_registration"], "status": "product-required", "summary": "A project grants an operational scope over stable subjects; scope is not ownership or attachment." },
{ "id": "device.collection.groups_subject", "from": ["device.collection"], "to": ["asset.asset", "device.device"], "status": "product-required", "summary": "A collection groups assets or devices independently from physical composition." },
{ "id": "device.tracking_device.is_device", "from": ["device.tracking_device"], "to": ["device.device"], "status": "product-required", "summary": "Tracking is a declared device role/capability, not the identity of the tracked Asset." },
{ "id": "device.asset_binding.attaches_device", "from": ["device.asset_binding"], "to": ["device.device", "device.tracking_device"], "status": "product-required", "summary": "A binding identifies the device participating in a time-qualified asset attachment." },
{ "id": "device.asset_binding.attaches_asset", "from": ["device.asset_binding"], "to": ["asset.asset"], "status": "product-required", "summary": "A binding identifies the asset participating in a time-qualified attachment." },
{ "id": "device.tracking_device.tracks_asset", "from": ["device.tracking_device"], "to": ["asset.asset"], "status": "product-required", "summary": "A tracking device tracks an asset only through an active Device Asset Binding carrying valid_from, valid_to and provenance." },
{ "id": "device.device.has_restricted_identifier", "from": ["device.device", "device.tracking_device"], "to": ["device.restricted_identifier"], "status": "source-confirmed", "summary": "Hardware and provider identifiers are classified restricted references on a device." },
{ "id": "device.device.uses_credential_reference", "from": ["device.device"], "to": ["device.credential_reference"], "status": "source-confirmed", "summary": "A device may use an opaque credential reference resolved only inside the owning secret boundary." },
{ "id": "device.adapter_package.has_version", "from": ["device.adapter_package"], "to": ["device.adapter_version"], "status": "source-confirmed", "summary": "An adapter package publishes immutable executable versions." },
{ "id": "device.model_profile.uses_adapter_version", "from": ["device.model_profile"], "to": ["device.adapter_version"], "status": "source-confirmed", "summary": "A model profile pins one approved adapter version and its capability contract." },
{ "id": "device.discovery.proposes_enrollment", "from": ["device.discovery"], "to": ["device.enrollment_intent"], "status": "source-confirmed", "summary": "Untrusted discovery evidence may seed an enrollment intent but cannot create ownership by itself." },
{ "id": "device.enrollment_intent.registers_device", "from": ["device.enrollment_intent"], "to": ["device.device", "device.tracking_device"], "status": "product-required", "summary": "An accepted enrollment intent creates or claims a stable managed device identity." },
{ "id": "device.enrollment_intent.uses_model_profile", "from": ["device.enrollment_intent"], "to": ["device.model_profile"], "status": "product-required", "summary": "Direct onboarding selects an approved model and adapter contract before traffic is accepted." },
{ "id": "device.edge_registration.runs_as_service_instance", "from": ["device.edge_registration"], "to": ["infrastructure.service_instance"], "status": "product-required", "summary": "A registered Edge is realized by an explicit service instance on infrastructure; Edge identity is not the host identity." },
{ "id": "device.edge_registration.exposes_route", "from": ["device.edge_registration"], "to": ["device.route"], "status": "product-required", "summary": "Only Core-approved Edge registrations can terminate device routes." },
{ "id": "device.route.uses_model_profile", "from": ["device.route"], "to": ["device.model_profile"], "status": "product-required", "summary": "A route resolves traffic through the device model and pinned adapter contract." },
{ "id": "device.session.uses_route", "from": ["device.session"], "to": ["device.route"], "status": "source-confirmed", "summary": "A runtime session is accepted only on one approved route." },
{ "id": "device.session.belongs_to_device", "from": ["device.session"], "to": ["device.device"], "status": "source-confirmed", "summary": "A session belongs to one stable device identity after route and enrollment checks." },
{ "id": "device.configuration_revision.applies_to_device", "from": ["device.configuration_revision"], "to": ["device.device"], "status": "product-required", "summary": "A desired configuration revision targets one managed device." },
{ "id": "device.configuration_state.reconciles_revision", "from": ["device.configuration_state"], "to": ["device.configuration_revision"], "status": "product-required", "summary": "Configuration state reconciles desired revision with observed evidence without mutating revision history." },
{ "id": "device.command_intent.targets_device", "from": ["device.command_intent"], "to": ["device.device"], "status": "product-required", "summary": "Every command intent resolves to one authorized stable device target." },
{ "id": "device.command_intent.uses_route", "from": ["device.command_intent"], "to": ["device.route"], "status": "product-required", "summary": "Typed command transport uses an approved route and never an arbitrary host shell." },
{ "id": "device.command_intent.has_event", "from": ["device.command_intent"], "to": ["device.command_event"], "status": "product-required", "summary": "Command lifecycle is evidenced by immutable events rather than assumed from dispatch." },
{ "id": "device.audit_event.records_subject", "from": ["device.audit_event"], "to": ["device.device", "device.asset_binding", "device.enrollment_intent", "device.configuration_revision", "device.command_intent", "device.edge_registration"], "status": "product-required", "summary": "Governed Device Core mutations and decisions emit audit evidence for their stable target." }
]
}
@@ -1,6 +1,6 @@
{
"version": "1.1.0",
"updatedAt": "2026-07-20",
"version": "1.2.0",
"updatedAt": "2026-08-22",
"sourceRoots": [
{
"surface": "gelios-rest",
@@ -57,6 +57,8 @@
"Do not promote operational_status, freshness, GPS quality, missing position or unknown fallback buckets to Gelios monitoring states. The official filter value contracts are signal_state active/inactive and movement_state moving/stopped only.",
"Do not invoke command send, create, update, delete or purge routes as ontology evidence.",
"Robot2B pilot counts (107 credential-visible units and 95 legacy snapshot units) are historical evidence only; canonical collection scope is dynamically all entities visible to the bound credential.",
"Do not bulk-download history, media or full geozone geometry before collection policy and storage architecture are approved."
"Do not bulk-download history, media or full geozone geometry before collection policy and storage architecture are approved.",
"Do not treat gelios.unit as Asset or gelios.tracker_device as Device Core Tracking Device without an explicit source binding.",
"New direct B2 onboarding must not require a Gelios connection; legacy/provider and direct observations retain distinct provenance."
]
}
@@ -1,6 +1,6 @@
{
"version": "1.1.0",
"updatedAt": "2026-07-20",
"version": "1.2.0",
"updatedAt": "2026-08-22",
"rules": [
{
"id": "guardrail.gelios.ontology_not_runtime_store",
@@ -32,6 +32,12 @@
"summary": "Cesium pins and labels must consume stable gelios.unit and normalized gelios.position_fix through map.moving_object. They must not use raw message IDs, transient Cesium entity IDs or provider credentials as map identity.",
"entityIds": ["gelios.unit", "gelios.position_fix", "gelios.telemetry_snapshot", "map.moving_object", "map.pin", "map.label"]
},
{
"id": "guardrail.gelios.neutral_composition_requires_explicit_binding",
"severity": "error",
"summary": "Gelios unit, tracker and position records may map to Asset, Tracking Device and Position Observation only through explicit versioned source bindings. Provider identities must not silently overwrite NODE.DC identities or direct-device evidence.",
"entityIds": ["gelios.unit", "gelios.tracker_device", "gelios.position_fix", "asset.asset", "device.tracking_device", "observation.position_observation"]
},
{
"id": "guardrail.gelios.monitoring_states_are_closed",
"severity": "error",
@@ -60,6 +66,9 @@
"blockedConflations": [
["gelios.integration", "gelios.access_scope"],
["gelios.unit", "gelios.tracker_device"],
["gelios.unit", "asset.asset"],
["gelios.tracker_device", "device.tracking_device"],
["gelios.position_fix", "observation.position_observation"],
["gelios.telemetry_snapshot", "gelios.raw_telemetry_message"],
["gelios.position_fix", "map.moving_object"],
["gelios.signal_state", "gelios.movement_state"],
@@ -1,7 +1,7 @@
{
"id": "gelios",
"version": "1.1.0",
"updatedAt": "2026-07-20",
"version": "1.2.0",
"updatedAt": "2026-08-22",
"status": "product-required/source-evidenced",
"summary": "Source-evidenced Gelios Pro telemetry, fleet, spatial, reporting and command-domain ontology. Unit monitoring exposes only the official signal and movement value contracts; consumers may not invent additional operational states."
"summary": "Source-evidenced Gelios provider ontology with explicit mappings into provider-neutral Asset, Tracking Device, Observation and Map contracts. Official signal and movement value contracts remain closed."
}
@@ -1,12 +1,16 @@
{
"version": "1.1.0",
"updatedAt": "2026-07-20",
"version": "1.2.0",
"updatedAt": "2026-08-22",
"relations": [
{ "id": "gelios.integration.is_provider_connection", "from": ["gelios.integration"], "to": ["integration.connection"], "status": "product-required", "summary": "A Gelios integration is a tenant-scoped instance of the common external provider connection contract." },
{ "id": "gelios.access_scope.specializes_integration_scope", "from": ["gelios.access_scope"], "to": ["integration.access_scope"], "status": "product-required", "summary": "Gelios credential-visible capability scope specializes the common provider access-scope contract." },
{ "id": "gelios.collection_run.is_integration_collection_run", "from": ["gelios.collection_run"], "to": ["integration.collection_run"], "status": "product-required", "summary": "Gelios collection runs inherit the bounded collection and audit lifecycle of external integrations." },
{ "id": "gelios.raw_message.is_integration_raw_envelope", "from": ["gelios.raw_telemetry_message"], "to": ["integration.raw_envelope"], "status": "product-required", "summary": "A Gelios raw message is a restricted provider envelope, not a UI read model." },
{ "id": "gelios.unit.is_integration_canonical_subject", "from": ["gelios.unit"], "to": ["integration.canonical_subject"], "status": "product-required", "summary": "A Gelios unit is a stable canonical subject produced by the provider adapter." },
{ "id": "gelios.unit.is_integration_canonical_subject", "from": ["gelios.unit"], "to": ["integration.canonical_subject"], "status": "product-required", "summary": "A Gelios unit is a stable provider subject produced by the provider adapter." },
{ "id": "gelios.unit.maps_to_asset", "from": ["gelios.unit"], "to": ["asset.asset"], "status": "product-required", "summary": "A Gelios unit may resolve to a NODE.DC Asset only through an explicit source binding. The provider unit id never becomes the Asset identity." },
{ "id": "gelios.tracker_device.maps_to_tracking_device", "from": ["gelios.tracker_device"], "to": ["device.tracking_device"], "status": "product-required", "summary": "A Gelios tracker record may resolve to a Device Core Tracking Device through an explicit restricted-identifier and source binding." },
{ "id": "gelios.telemetry_snapshot.maps_to_observation", "from": ["gelios.telemetry_snapshot"], "to": ["observation.observation"], "status": "product-required", "summary": "A versioned field mapping may publish provider telemetry as provider-neutral observations while retaining Gelios provenance and exact value contracts." },
{ "id": "gelios.position_fix.maps_to_position_observation", "from": ["gelios.position_fix"], "to": ["observation.position_observation"], "status": "product-required", "summary": "A Gelios position fix may produce a provider-neutral position observation for the explicitly bound Asset and Tracking Device." },
{ "id": "gelios.command_template.is_integration_command_capability", "from": ["gelios.command_template"], "to": ["integration.command_capability"], "status": "product-required", "summary": "Gelios command templates are catalogued provider command capabilities only." },
{ "id": "gelios.command_dispatch.is_integration_command_intent", "from": ["gelios.command_dispatch"], "to": ["integration.command_intent"], "status": "future-concept", "summary": "Any future Gelios command dispatch must become an explicitly governed generic command intent." },
{ "id": "gelios.command_audit.is_integration_command_audit", "from": ["gelios.command_audit"], "to": ["integration.command_audit"], "status": "future-concept", "summary": "Gelios command audit specializes the generic red-domain audit contract." },
@@ -39,8 +43,8 @@
{ "id": "gelios.command_dispatch.targets_scope", "from": ["gelios.command_dispatch"], "to": ["gelios.unit", "gelios.unit_group", "gelios.access_scope"], "status": "future-concept", "summary": "A future dispatch target must resolve to an approved unit or explicitly approved group in scope." },
{ "id": "gelios.command_dispatch.has_delivery", "from": ["gelios.command_dispatch"], "to": ["gelios.command_delivery"], "status": "future-concept", "summary": "A future dispatch is associated with delivery/task state rather than assumed successful on request creation." },
{ "id": "gelios.command_dispatch.has_audit", "from": ["gelios.command_dispatch"], "to": ["gelios.command_audit"], "status": "future-concept", "summary": "Every future dispatch requires immutable user-confirmation and outcome audit." },
{ "id": "gelios.unit.is_map_moving_object", "from": ["gelios.unit"], "to": ["map.moving_object"], "status": "product-required", "summary": "A valid credential-visible Gelios unit is exposed through the provider-neutral moving-object contract, never as a renderer entity identity." },
{ "id": "gelios.position_fix.positions_map_subject", "from": ["gelios.position_fix"], "to": ["map.moving_object"], "status": "product-required", "summary": "A normalized position fix updates the stable map subject bound to the Gelios unit." },
{ "id": "gelios.unit.is_map_moving_object", "from": ["gelios.unit"], "to": ["map.moving_object"], "status": "product-required", "summary": "Legacy/provider Data Products may expose a credential-visible Gelios unit through the moving-object contract. New composed products bind the unit to Asset and use the Asset as stable map subject." },
{ "id": "gelios.position_fix.positions_map_subject", "from": ["gelios.position_fix"], "to": ["map.moving_object"], "status": "product-required", "summary": "Legacy/provider Data Products may position their Gelios moving-object subject; new composed products map the fix to a position observation targeting the bound Asset." },
{ "id": "gelios.geozone.is_map_zone", "from": ["gelios.geozone"], "to": ["map.zone"], "status": "product-required", "summary": "An approved Gelios geozone is exposed to the map through the generic zone contract." },
{ "id": "gelios.geopoint.is_map_place_target", "from": ["gelios.geopoint"], "to": ["map.place_target"], "status": "product-required", "summary": "An approved Gelios geopoint is exposed through a provider-neutral place target." }
]
@@ -0,0 +1,15 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"aliases": [
{ "alias": "VPS host", "canonicalId": "infrastructure.host" },
{ "alias": "внешний VPS", "canonicalId": "infrastructure.host" },
{ "alias": "инфраструктурный хост", "canonicalId": "infrastructure.host" },
{ "alias": "сетевой endpoint хоста", "canonicalId": "infrastructure.endpoint" },
{ "alias": "развёртывание сервиса", "canonicalId": "infrastructure.deployment" },
{ "alias": "экземпляр сервиса на VPS", "canonicalId": "infrastructure.service_instance" },
{ "alias": "ревизия конфигурации инфраструктуры", "canonicalId": "infrastructure.configuration_revision" },
{ "alias": "ссылка на SSH credential", "canonicalId": "infrastructure.management_credential_reference" },
{ "alias": "браузерная консоль VPS", "canonicalId": "infrastructure.management_session" }
]
}
@@ -0,0 +1,13 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"entities": [
{ "id": "infrastructure.host", "name": "Infrastructure Host", "surface": "infrastructure", "status": ["product-required", "source-evidenced"], "authority": "Device Core infrastructure inventory", "summary": "Stable compute host such as a VPS, bare-metal server or local node, independent from provider account, endpoint, deployment and running services." },
{ "id": "infrastructure.endpoint", "name": "Infrastructure Endpoint", "surface": "infrastructure", "status": ["product-required", "source-evidenced"], "authority": "Device Core infrastructure inventory", "summary": "Network-reachable address and protocol surface exposed by a host or service under an explicit visibility and trust policy." },
{ "id": "infrastructure.deployment", "name": "Infrastructure Deployment", "surface": "infrastructure", "status": ["product-required", "source-evidenced"], "authority": "Owning deploy system", "summary": "Auditable delivery of a versioned service/configuration artifact to a target host; it is not the running process or host identity." },
{ "id": "infrastructure.service_instance", "name": "Infrastructure Service Instance", "surface": "infrastructure", "status": ["product-required", "source-evidenced"], "authority": "Device Core infrastructure inventory", "summary": "Concrete running instance of a service on a host, linked to deployment evidence and current observations." },
{ "id": "infrastructure.configuration_revision", "name": "Infrastructure Configuration Revision", "surface": "infrastructure", "status": ["product-required"], "authority": "Owning deploy/configuration system", "summary": "Immutable desired infrastructure configuration revision referenced by deployments without embedding secrets." },
{ "id": "infrastructure.management_credential_reference", "name": "Infrastructure Management Credential Reference", "surface": "infrastructure-access", "status": ["product-required"], "authority": "Owning secret authority", "summary": "Opaque reference to a host-management credential; no secret value is stored in Ontology Core, Device Manager browser state or Data Products." },
{ "id": "infrastructure.management_session", "name": "Infrastructure Management Session", "surface": "infrastructure-access", "status": ["product-required"], "authority": "Owning privileged-access service", "summary": "Time-bounded, authorized and audited management channel to a host. Interactive console use is a privileged break-glass operation, not a standard device command." }
]
}
@@ -0,0 +1,19 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"sourceRoots": [
{ "surface": "device-core", "path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_DEVICE_CORE", "mode": "Read-only Edge registration, endpoint, trust, route and manager health source inspection" },
{ "surface": "device-edge-adr", "path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_DEVICE_CORE/docs/ADR_0001_CORE_INITIATED_EDGE_CHANNEL.md", "mode": "Replaceable untrusted VPS and Core-initiated Edge channel boundary" },
{ "surface": "ops-decision", "path": "DCPLATFORM-77", "mode": "Accepted ontology-first VPS inventory, monitoring and management boundary" }
],
"ledgers": [
{ "id": "ledger.infrastructure_domain_v0", "path": "docs/INFRASTRUCTURE_DOMAIN_ONTOLOGY.md", "entityIds": ["infrastructure.host", "infrastructure.endpoint", "infrastructure.deployment", "infrastructure.service_instance", "infrastructure.configuration_revision", "infrastructure.management_credential_reference", "infrastructure.management_session"] }
],
"baselineDocs": ["docs/INFRASTRUCTURE_DOMAIN_ONTOLOGY.md"],
"restrictions": [
"Do not treat integration.connection as a VPS or host inventory record.",
"Do not copy passwords, private keys, host tokens or console transcripts into Ontology Core.",
"Do not expose arbitrary host shell through the safe Device Core command channel.",
"Do not project hosts on a map without actual spatial evidence."
]
}
@@ -0,0 +1,51 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"rules": [
{
"id": "guardrail.infrastructure.host_is_not_connection",
"severity": "error",
"summary": "A VPS or server is an Infrastructure Host. A provider account connection, network endpoint, deployment or Edge registration may reference it but must not replace its identity.",
"entityIds": ["infrastructure.host", "integration.connection", "infrastructure.endpoint", "infrastructure.deployment", "device.edge_registration"]
},
{
"id": "guardrail.infrastructure.runtime_layers_remain_distinct",
"severity": "error",
"summary": "Host, endpoint, deployment, service instance and Edge registration are separate graph subjects with explicit relations and independent lifecycle evidence.",
"entityIds": ["infrastructure.host", "infrastructure.endpoint", "infrastructure.deployment", "infrastructure.service_instance", "device.edge_registration"]
},
{
"id": "guardrail.infrastructure.management_is_break_glass",
"severity": "error",
"summary": "Interactive host management requires an app-owned privileged session with explicit actor, target, reason, expiry and audit. Device safe commands may never proxy arbitrary shell access.",
"entityIds": ["infrastructure.host", "infrastructure.management_credential_reference", "infrastructure.management_session", "device.command_intent", "device.audit_event"]
},
{
"id": "guardrail.infrastructure.secrets_never_reach_ontology_or_browser",
"severity": "error",
"summary": "Ontology, Data Products and browser state may carry only opaque secret references and safe metadata; credentials, private keys and host tokens remain in their owning secret authority.",
"entityIds": ["infrastructure.management_credential_reference", "device.credential_reference", "agent.token"]
},
{
"id": "guardrail.infrastructure.map_requires_spatial_evidence",
"severity": "error",
"summary": "A host or service may appear on a map only when a Data Product supplies real time-qualified spatial evidence. Otherwise Foundry must use a logical graph or Inspector presentation.",
"entityIds": ["infrastructure.host", "infrastructure.service_instance", "observation.position_observation", "map.moving_object", "map.place_target"]
},
{
"id": "guardrail.infrastructure.ontology_not_inventory_store",
"severity": "error",
"summary": "Ontology Core defines infrastructure meanings; Device Core or another owning inventory stores hosts, deployments, service instances, observations, credentials references and management-session audit.",
"entityIds": ["infrastructure.host", "infrastructure.deployment", "infrastructure.service_instance", "infrastructure.management_session"]
}
],
"blockedConflations": [
["infrastructure.host", "integration.connection"],
["infrastructure.host", "infrastructure.endpoint"],
["infrastructure.host", "infrastructure.deployment"],
["infrastructure.deployment", "infrastructure.service_instance"],
["infrastructure.service_instance", "device.edge_registration"],
["infrastructure.management_credential_reference", "infrastructure.management_session"],
["infrastructure.host", "map.moving_object"]
]
}
@@ -0,0 +1,7 @@
{
"id": "infrastructure",
"version": "0.1.0",
"updatedAt": "2026-08-22",
"status": "product-required/source-evidenced",
"summary": "Provider-neutral hosts, endpoints, deployments, service instances, configuration revisions and governed management access for Device Core Edge infrastructure."
}
@@ -0,0 +1,16 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"relations": [
{ "id": "infrastructure.host.exposes_endpoint", "from": ["infrastructure.host"], "to": ["infrastructure.endpoint"], "status": "product-required", "summary": "A host exposes only explicitly inventoried network endpoints with declared protocol and visibility." },
{ "id": "infrastructure.deployment.targets_host", "from": ["infrastructure.deployment"], "to": ["infrastructure.host"], "status": "product-required", "summary": "A deployment records its target host without becoming that host." },
{ "id": "infrastructure.deployment.uses_configuration_revision", "from": ["infrastructure.deployment"], "to": ["infrastructure.configuration_revision"], "status": "product-required", "summary": "A deployment pins an immutable desired configuration revision and secret references only." },
{ "id": "infrastructure.service_instance.runs_on_host", "from": ["infrastructure.service_instance"], "to": ["infrastructure.host"], "status": "product-required", "summary": "A concrete service instance runs on one inventoried host for a bounded lifecycle interval." },
{ "id": "infrastructure.service_instance.realizes_deployment", "from": ["infrastructure.service_instance"], "to": ["infrastructure.deployment"], "status": "product-required", "summary": "A service instance is reconciled with the deployment that produced its current release." },
{ "id": "infrastructure.service_instance.exposes_endpoint", "from": ["infrastructure.service_instance"], "to": ["infrastructure.endpoint"], "status": "product-required", "summary": "A service may expose an endpoint distinct from the host's management endpoint." },
{ "id": "infrastructure.host.uses_management_credential_reference", "from": ["infrastructure.host"], "to": ["infrastructure.management_credential_reference"], "status": "product-required", "summary": "Host management resolves an opaque credential reference inside the owning privileged service." },
{ "id": "infrastructure.management_session.targets_host", "from": ["infrastructure.management_session"], "to": ["infrastructure.host"], "status": "product-required", "summary": "A management session targets one explicit host and records actor, reason, expiry and audit correlation." },
{ "id": "infrastructure.management_session.uses_credential_reference", "from": ["infrastructure.management_session"], "to": ["infrastructure.management_credential_reference"], "status": "product-required", "summary": "A privileged session resolves a credential reference without returning the secret to the browser." },
{ "id": "infrastructure.host.belongs_to_device_project", "from": ["infrastructure.host"], "to": ["device.project"], "status": "product-required", "summary": "A Device Core project may scope a host while host identity remains independent from the project." }
]
}
@@ -0,0 +1,12 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"aliases": [
{ "alias": "наблюдение устройства", "canonicalId": "observation.observation" },
{ "alias": "наблюдаемое свойство", "canonicalId": "observation.observed_property" },
{ "alias": "источник наблюдения", "canonicalId": "observation.source" },
{ "alias": "политика свежести наблюдения", "canonicalId": "observation.freshness_policy" },
{ "alias": "проверка здоровья VPS", "canonicalId": "observation.health_observation" },
{ "alias": "позиция актива", "canonicalId": "observation.position_observation" }
]
}
@@ -0,0 +1,12 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"entities": [
{ "id": "observation.observation", "name": "Observation", "surface": "observation", "status": ["product-required"], "authority": "Owning telemetry or monitoring service", "summary": "Provider-neutral time-qualified statement about one declared property of one stable subject, carrying observed/received time, provenance and quality metadata." },
{ "id": "observation.observed_property", "name": "Observed Property", "surface": "observation", "status": ["product-required"], "authority": "Owning domain + Ontology Core", "summary": "Versioned semantic property contract declaring value type, unit, allowed values and interpretation independently from a provider field name." },
{ "id": "observation.source", "name": "Observation Source", "surface": "observation", "status": ["product-required"], "authority": "Owning telemetry or monitoring service", "summary": "Declared producer or adapter provenance for an observation, including provider-neutral source identity and mapping version." },
{ "id": "observation.freshness_policy", "name": "Observation Freshness Policy", "surface": "observation", "status": ["product-required"], "authority": "Consuming Data Product", "summary": "Explicit TTL, grace and missing-data interpretation selected by a consuming contract; freshness is not part of the observed value itself." },
{ "id": "observation.health_observation", "name": "Health Observation", "surface": "observation", "status": ["product-required", "source-evidenced"], "authority": "Owning monitoring service", "summary": "Time-qualified health or reachability observation for a host, service, Edge or device, retaining check kind and evidence rather than mutating the subject identity." },
{ "id": "observation.position_observation", "name": "Position Observation", "surface": "observation", "status": ["product-required", "source-evidenced"], "authority": "Owning telemetry service", "summary": "Time-qualified spatial observation with coordinate reference, location, quality and provenance that may project an Asset into a map Data Product." }
]
}
@@ -0,0 +1,20 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"sourceRoots": [
{ "surface": "device-core", "path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_DEVICE_CORE", "mode": "Read-only session, route, current health and direct-device evidence inspection" },
{ "surface": "gelios", "path": "catalog/domain-packages/gelios", "mode": "Existing provider telemetry, position and closed monitoring-state contracts" },
{ "surface": "foundry", "path": "foundry://application/1c7dcdbb-6e50-4272-b1bc-aa5ece77ae99", "mode": "Robot2B Data Product semantic facets and moving-object projection inspection" },
{ "surface": "ops-decision", "path": "DCPLATFORM-77", "mode": "Accepted VPS health and provider-neutral observation boundary" }
],
"ledgers": [
{ "id": "ledger.observation_domain_v0", "path": "docs/OBSERVATION_DOMAIN_ONTOLOGY.md", "entityIds": ["observation.observation", "observation.observed_property", "observation.source", "observation.freshness_policy", "observation.health_observation", "observation.position_observation"] }
],
"baselineDocs": ["docs/OBSERVATION_DOMAIN_ONTOLOGY.md"],
"restrictions": [
"Do not copy live telemetry, monitoring histories, positions or raw provider payloads into Ontology Core.",
"Do not add consumer fallback statuses that are absent from the declared observed-property contract.",
"Do not treat missing evidence as unhealthy without an explicit freshness and missing-value rule.",
"Do not project a subject to a map without source-backed spatial evidence."
]
}
@@ -0,0 +1,50 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"rules": [
{
"id": "guardrail.observation.value_time_and_freshness_are_separate",
"severity": "error",
"summary": "Observed value, observed_at, received_at, provenance and freshness evaluation are separate contract fields. Consumers must not invent fallback status from a missing timestamp.",
"entityIds": ["observation.observation", "observation.observed_property", "observation.source", "observation.freshness_policy"]
},
{
"id": "guardrail.observation.missing_is_not_automatically_unhealthy",
"severity": "error",
"summary": "Missing or expired evidence is unavailable/stale according to an explicit policy; it must not be silently converted to an unhealthy value unless the property contract says so.",
"entityIds": ["observation.health_observation", "observation.freshness_policy", "observation.observed_property"]
},
{
"id": "guardrail.observation.provider_values_require_mapping",
"severity": "error",
"summary": "Provider fields and status values enter Data Products only through a versioned mapping to an observed-property value contract; Foundry and renderers may not classify raw values.",
"entityIds": ["observation.observation", "observation.observed_property", "observation.source", "gelios.signal_state", "gelios.movement_state", "map.state_facet"]
},
{
"id": "guardrail.observation.not_lifecycle_or_command",
"severity": "error",
"summary": "An observation reports evidence. It does not replace immutable lifecycle events, desired configuration, command intent or command outcome.",
"entityIds": ["observation.observation", "asset.lifecycle_event", "device.configuration_state", "device.command_intent", "device.command_event"]
},
{
"id": "guardrail.observation.position_requires_provenance",
"severity": "error",
"summary": "Map projection requires coordinate reference, observed time, source, subject binding and quality/provenance. A host or asset display label alone is not spatial evidence.",
"entityIds": ["observation.position_observation", "observation.source", "asset.asset", "infrastructure.host", "map.moving_object"]
},
{
"id": "guardrail.observation.ontology_not_telemetry_store",
"severity": "error",
"summary": "Ontology Core defines observation meanings and value contracts; it must not store live telemetry, health checks, positions, histories or monitoring snapshots.",
"entityIds": ["observation.observation", "observation.health_observation", "observation.position_observation"]
}
],
"blockedConflations": [
["observation.observation", "observation.observed_property"],
["observation.observation", "observation.source"],
["observation.health_observation", "infrastructure.host"],
["observation.health_observation", "infrastructure.service_instance"],
["observation.position_observation", "asset.asset"],
["observation.position_observation", "map.moving_object"]
]
}
@@ -0,0 +1,7 @@
{
"id": "observation",
"version": "0.1.0",
"updatedAt": "2026-08-22",
"status": "product-required/source-evidenced",
"summary": "Provider-neutral time-qualified observations, properties, sources, freshness policy, health and position evidence for assets, devices and infrastructure."
}
@@ -0,0 +1,16 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"relations": [
{ "id": "observation.observation.observes_subject", "from": ["observation.observation"], "to": ["asset.asset", "device.device", "device.tracking_device", "infrastructure.host", "infrastructure.service_instance", "device.edge_registration"], "status": "product-required", "summary": "An observation targets one stable subject without becoming that subject or its lifecycle state." },
{ "id": "observation.observation.observes_property", "from": ["observation.observation"], "to": ["observation.observed_property"], "status": "product-required", "summary": "Each observation declares the semantic property and value contract it evaluates." },
{ "id": "observation.observation.produced_by_source", "from": ["observation.observation"], "to": ["observation.source"], "status": "product-required", "summary": "Every observation records source and mapping provenance." },
{ "id": "observation.observation.evaluated_by_freshness", "from": ["observation.observation"], "to": ["observation.freshness_policy"], "status": "product-required", "summary": "Consumers evaluate observed and received time against an explicit freshness contract outside the raw value." },
{ "id": "observation.health_observation.is_observation", "from": ["observation.health_observation"], "to": ["observation.observation"], "status": "product-required", "summary": "Health evidence uses the common time, subject, property, source and freshness contract." },
{ "id": "observation.health_observation.targets_infrastructure", "from": ["observation.health_observation"], "to": ["infrastructure.host", "infrastructure.service_instance", "device.edge_registration", "device.device"], "status": "product-required", "summary": "A health check can target a host, running service, Edge registration or device without collapsing those layers." },
{ "id": "observation.position_observation.is_observation", "from": ["observation.position_observation"], "to": ["observation.observation"], "status": "product-required", "summary": "Position evidence uses the common observation provenance and time contract." },
{ "id": "observation.position_observation.positions_asset", "from": ["observation.position_observation"], "to": ["asset.asset"], "status": "product-required", "summary": "A position observation locates the stable Asset currently tracked by its attached device." },
{ "id": "observation.position_observation.projects_map_subject", "from": ["observation.position_observation"], "to": ["map.moving_object", "map.place_target"], "status": "product-required", "summary": "Foundry may project position evidence through a declared map semantic binding; the observation is not a renderer entity." },
{ "id": "observation.source.uses_device_session", "from": ["observation.source"], "to": ["device.session"], "status": "product-required", "summary": "Direct device observations may retain the accepted Device Plane session as provenance." }
]
}
@@ -0,0 +1,14 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"aliases": [
{ "alias": "business party", "canonicalId": "party.party" },
{ "alias": "участник предметной области", "canonicalId": "party.party" },
{ "alias": "физическое лицо", "canonicalId": "party.person" },
{ "alias": "юридическое лицо", "canonicalId": "party.organization" },
{ "alias": "учётная запись домена", "canonicalId": "party.account" },
{ "alias": "субъект доступа домена", "canonicalId": "party.access_subject" },
{ "alias": "контактная точка", "canonicalId": "party.contact_point" },
{ "alias": "роль водителя", "canonicalId": "party.driver_role" }
]
}
@@ -0,0 +1,13 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"entities": [
{ "id": "party.party", "name": "Party", "surface": "domain", "status": ["product-required"], "authority": "Owning product domain", "summary": "Stable provider-neutral actor identity that may be a person or organization and can participate in ownership, responsibility, access and operational assignments." },
{ "id": "party.person", "name": "Person", "surface": "domain", "status": ["product-required"], "authority": "Owning product domain", "summary": "Natural person represented independently from a HUB user, authentication identity, account or driver assignment." },
{ "id": "party.organization", "name": "Organization", "surface": "domain", "status": ["product-required"], "authority": "Owning product domain", "summary": "Legal or operational organization represented independently from a HUB client context, provider connection or project." },
{ "id": "party.account", "name": "Account", "surface": "domain", "status": ["product-required"], "authority": "Owning application", "summary": "Application- or provider-scoped account projection that represents a party but is not the party, credential or authentication session." },
{ "id": "party.access_subject", "name": "Access Subject", "surface": "access", "status": ["product-required"], "authority": "Owning access service", "summary": "Stable subject evaluated by an owning access service. It may bind to a person, organization or service actor without replacing the source authority identity." },
{ "id": "party.contact_point", "name": "Contact Point", "surface": "domain", "status": ["product-required"], "authority": "Owning product domain", "summary": "Classified communication endpoint such as email or phone, subject to field and exposure policy." },
{ "id": "party.driver_role", "name": "Driver Role", "surface": "domain", "status": ["product-required"], "authority": "Owning operations domain", "summary": "Time-qualified operational role through which a person may be assigned to operate an asset; it is not a permanent subtype of person." }
]
}
@@ -0,0 +1,17 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"sourceRoots": [
{ "surface": "hub", "path": "/Users/dcconstructions/Downloads/mnt/NODEDC/platform/services/launcher", "mode": "Read-only inspection of user, membership, client-context and application-access authority boundaries" },
{ "surface": "ops-decision", "path": "DCPLATFORM-70", "mode": "Accepted provider-neutral person, driver, account, access and moving-object composition decision" }
],
"ledgers": [
{ "id": "ledger.party_domain_v0", "path": "docs/PARTY_ASSET_DOMAIN_ONTOLOGY.md", "entityIds": ["party.party", "party.person", "party.organization", "party.account", "party.access_subject", "party.contact_point", "party.driver_role"] }
],
"baselineDocs": ["docs/PARTY_ASSET_DOMAIN_ONTOLOGY.md"],
"restrictions": [
"Do not copy credentials, authentication sessions, raw PII or app-owned permission state into Ontology Core.",
"Do not infer a person or organization identity from display text alone.",
"Do not turn driver, owner or operator into permanent type hierarchies when a time-qualified assignment is required."
]
}
@@ -0,0 +1,38 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"rules": [
{
"id": "guardrail.party.source_identities_remain_distinct",
"severity": "error",
"summary": "Person, organization, account, HUB user, authentication identity and agent identity remain distinct records joined only by explicit source-evidenced bindings.",
"entityIds": ["party.person", "party.organization", "party.account", "hub.user", "ndcauth.identity", "agent.identity"]
},
{
"id": "guardrail.party.access_enforcement_stays_with_owner",
"severity": "error",
"summary": "Ontology may describe an access subject and role meaning but must not store credentials or replace HUB, NDCAuth or app-owned authorization enforcement.",
"entityIds": ["party.access_subject", "hub.membership", "hub.app_grant", "ndcauth.identity"]
},
{
"id": "guardrail.party.roles_are_time_qualified",
"severity": "error",
"summary": "Operational roles such as driver are assignments with validity intervals and provenance, not permanent person subtypes or mutable labels on an asset.",
"entityIds": ["party.person", "party.driver_role", "asset.assignment", "asset.asset"]
},
{
"id": "guardrail.party.contacts_require_field_policy",
"severity": "warning",
"summary": "Phone, email and other contact values require an explicit field and exposure policy before Data Product, Foundry or external publication.",
"entityIds": ["party.party", "party.contact_point"]
}
],
"blockedConflations": [
["party.person", "hub.user"],
["party.person", "ndcauth.identity"],
["party.organization", "hub.client_context"],
["party.account", "integration.connection"],
["party.access_subject", "agent.identity"],
["party.person", "party.driver_role"]
]
}
@@ -0,0 +1,7 @@
{
"id": "party",
"version": "0.1.0",
"updatedAt": "2026-08-22",
"status": "product-required/source-evidenced",
"summary": "Provider-neutral actors, people, organizations, accounts, access subjects, contacts and operational roles composed without conflating product identities or authentication records."
}
@@ -0,0 +1,13 @@
{
"version": "0.1.0",
"updatedAt": "2026-08-22",
"relations": [
{ "id": "party.person.is_party", "from": ["party.person"], "to": ["party.party"], "status": "product-required", "summary": "A person participates in cross-domain relations through a stable Party identity." },
{ "id": "party.organization.is_party", "from": ["party.organization"], "to": ["party.party"], "status": "product-required", "summary": "An organization participates in cross-domain relations through a stable Party identity." },
{ "id": "party.account.represents_party", "from": ["party.account"], "to": ["party.party"], "status": "product-required", "summary": "An account explicitly represents a party inside one owning application or provider boundary." },
{ "id": "party.access_subject.represents_party", "from": ["party.access_subject"], "to": ["party.party"], "status": "product-required", "summary": "An access subject may resolve to a party while enforcement remains in the owning access service." },
{ "id": "party.party.has_contact_point", "from": ["party.party"], "to": ["party.contact_point"], "status": "product-required", "summary": "A party may expose classified contact points under an explicit field policy." },
{ "id": "party.driver_role.assigned_to_person", "from": ["party.driver_role"], "to": ["party.person"], "status": "product-required", "summary": "A driver role is assigned to a person for a bounded operational interval." },
{ "id": "party.driver_role.operates_asset", "from": ["party.driver_role"], "to": ["asset.asset"], "status": "product-required", "summary": "A driver role operates an asset through an explicit time-qualified assignment rather than a permanent hierarchy." }
]
}
@@ -0,0 +1,115 @@
# Device Domain Ontology v0.1
Date: 2026-08-22
Status: product-required / source-evidenced baseline for Device Core direct onboarding and Device Plane operation.
## Purpose
This package gives Device Core a provider-neutral semantic contract for registering, composing, connecting, configuring, observing and governing devices. It is derived from the existing Device Core schemas and Device Plane contracts, but it does not copy runtime tables into Ontology Core.
New B2 trackers are onboarded directly through Device Core. Gelios remains a parallel legacy/provider source for already connected units and is not a prerequisite for new devices.
## Runtime authority
Device Core owns:
- owner/project and collection scope;
- stable device identities;
- restricted identifiers and opaque credential references;
- adapter packages, immutable adapter versions and model profiles;
- discovery and enrollment lifecycle;
- time-qualified Asset bindings;
- Edge registrations, routes and sessions;
- desired configuration revisions and reconciled state;
- typed command intents/events and audit;
- device observations and their publication to Data Products.
Ontology Core owns only meanings, relation vocabulary, guardrails and evidence routing. Foundry consumes approved Data Products. Neither Ontology Core nor Foundry executes enrollment, device traffic or commands.
## Source evidence mapping
The first package is source-evidenced by the existing Device Core migrations:
- migration 001: profiles, contours, discoveries, device instances, bindings and audit;
- migration 002: owner scopes, projects, collections and grants;
- migration 004: adapter packages/versions, model profiles, Edge registrations, routes, sessions and enrollment intents;
- migration 006: direct lifecycle and ownership transitions;
- migration 008: restricted identifiers and credential bindings;
- migration 010: resource bindings, configuration revisions/state, commands and events;
- migration 013: Edge endpoint, server name, trust, certificate identity, generation and lifecycle;
- migration 015: integration device id retained as a restricted external reference.
Table names remain implementation details. Canonical entity ids remain stable even if runtime storage is refactored.
## Direct B2 onboarding
The accepted lifecycle is:
```text
candidate evidence
└─▶ device.discovery
└─▶ authorized device.enrollment_intent
├─▶ device.device / device.tracking_device
├─▶ device.model_profile ─▶ device.adapter_version
├─▶ device.restricted_identifier (IMEI/serial/provider id)
└─▶ device.asset_binding ─▶ asset.asset
accepted traffic
└─▶ device.edge_registration
└─▶ device.route
└─▶ device.session
└─▶ observation.source / observations
```
Discovery never claims ownership. Enrollment requires an authorized owner/project, reviewed model profile and exact identifier policy. The B2-to-trike binding is independent and temporal; a device may exist before attachment, and an Asset may exist without a current tracker.
## Adapter boundary
Vendor/protocol specifics live behind `device.adapter_package`, immutable `device.adapter_version` and `device.model_profile`. A future open-source tracking or traffic stack may implement adapters, storage or protocol processing behind this boundary. Its tables and vendor vocabulary do not become product ontology.
## Edge channel
The canonical Edge channel remains Core-initiated and pinned-mTLS:
- Core approves Edge registration identity, endpoint, server name and trust material;
- Edge is replaceable and untrusted for business authority;
- a concrete `infrastructure.service_instance` realizes the Edge registration on a host;
- routes terminate only at an approved Edge registration;
- sessions bind a stable device to one approved route;
- Edge never owns the central business database, secret authority or arbitrary command policy.
## Configuration
`device.configuration_revision` is immutable desired state. `device.configuration_state` reconciles a desired revision with observed evidence and time. A new revision never rewrites the old one. Secrets remain opaque references and are resolved only by the owning service.
## Commands
Commands are a red domain:
- target is a stable Device id;
- command type and payload schema are declared by the approved model/adapter contract;
- authorization, TTL and idempotency are explicit;
- request, acceptance, dispatch, acknowledgement, expiry and failure are separate events;
- arbitrary shell, raw provider text, firmware, reboot or memory wipe is not implied by the current safe `typed-service-ping-v1` contract;
- Device Control Core is authoritative for accepting ACK and final outcome.
## Gelios coexistence
Provider records map explicitly:
- `gelios.tracker_device``device.tracking_device` through a source binding;
- `gelios.unit``asset.asset` through a source binding;
- `gelios.position_fix``observation.position_observation` through a versioned mapping;
- legacy signal/movement values remain their existing closed Gelios value contracts.
Provider collection and direct Device traffic may coexist. They must retain source provenance and must not silently merge conflicting identities.
## Acceptance
- New B2 can be enrolled without a Gelios connection.
- Trike Asset and B2 Device receive different stable ids.
- IMEI remains restricted and can be rotated/reconciled without replacing Asset identity.
- Traffic is rejected before enrollment/profile/route/Edge checks.
- Config revisions and command lifecycle are immutable/auditable.
- Runtime records stay in Device Core, not Ontology Core or Foundry.
@@ -2,7 +2,20 @@
Package: `catalog/domain-packages/gelios`
Status: `v1.1.0`, source-evidenced and product-required.
Status: `v1.2.0`, source-evidenced and product-required.
## Provider-neutral Asset and Device bridge
As of 2026-08-22 Gelios remains a provider/legacy domain and no longer acts as the implicit product identity model for new devices:
- `gelios.unit` may map to `asset.asset` only through an explicit versioned source binding;
- `gelios.tracker_device` may map to `device.tracking_device` only through an explicit restricted-identifier/source binding;
- `gelios.position_fix` may map to `observation.position_observation` while retaining source and mapping provenance;
- existing provider Data Products may continue to use their current `map.moving_object` projection;
- new composed Data Products use the Asset as the stable map subject and the tracker as the observation source;
- direct B2 onboarding through Device Core does not require a Gelios connection.
Provider ids, IMEI values and renderer ids never replace NODE.DC Asset or Device identity. Direct and Gelios observations may coexist but must not silently overwrite one another.
## Purpose and boundary
@@ -0,0 +1,114 @@
# Infrastructure Domain Ontology v0.1
Date: 2026-08-22
Status: product-required / source-evidenced baseline for Device Core infrastructure inventory and Edge topology.
## Purpose
This package models VPS and other compute infrastructure as a provider-neutral graph. It prevents the current UI requirement from becoming a one-off VPS form or a false extension of `integration.connection`.
The canonical layers are:
```text
infrastructure.host
├─exposes──────────────▶ infrastructure.endpoint
├─target_of────────────▶ infrastructure.deployment
├─runs─────────────────▶ infrastructure.service_instance
└─target_of────────────▶ infrastructure.management_session
infrastructure.deployment
└─uses─────────────────▶ infrastructure.configuration_revision
infrastructure.service_instance
├─realizes─────────────▶ infrastructure.deployment
├─exposes──────────────▶ infrastructure.endpoint
└─realizes─────────────▶ device.edge_registration
```
These are peer graph subjects, not a deep type hierarchy. Each has independent identity and lifecycle.
## Authority boundary
Device Core may own the operational inventory required for Device Plane hosts, Edge services, monitoring and configuration. The canonical deploy runner remains authoritative for NODE.DC Synology release application and acceptance. A future provider adapter may own VPS account provisioning. Ontology Core owns none of those runtime mutations.
`integration.connection` represents a provider account/capability connection. It is never a Host. A cloud account may expose many Hosts; one Host may run many Service Instances; one Deployment may create or update one or more instances.
## Host registration
A host record requires at minimum:
- stable NODE.DC host id;
- host kind/capability profile;
- project/owner scope;
- provider-neutral display metadata;
- one or more namespaced external references where available;
- explicit endpoint records with protocol, visibility and trust policy;
- secret references only, never credential values;
- lifecycle state and audit evidence.
Registration alone does not prove reachability or health. Those are observations.
## Deployment and service instance
`infrastructure.deployment` records delivery intent and outcome for a versioned artifact/configuration against a target Host. `infrastructure.service_instance` is the concrete running service reconciled to that deployment.
For Device Plane:
- an Edge container/process is a Service Instance;
- its Core-approved application identity is `device.edge_registration`;
- the VPS is the Host;
- its mTLS/HTTP endpoint is an Endpoint;
- the release action and source version are Deployment evidence.
None of those identifiers are interchangeable.
## Monitoring
Host and service monitoring uses `observation.health_observation` with:
- stable target id;
- check/property id;
- observed and received time;
- source and mapping version;
- value and evidence summary;
- freshness policy selected by the consuming Data Product.
No observation means unavailable evidence. It is not automatically equivalent to unhealthy. Host health, service health, Edge registration state and device transport state remain separate facets.
## Management session and console
Browser console is possible only as an app-owned privileged management session, not as an iframe with an SSH key and not as a Device command.
Required contract:
- explicit authorized actor and project scope;
- exact target Host;
- reason/ticket or approved workflow context;
- short expiry and revocation;
- server-side credential resolution through an opaque reference;
- no secret returned to the browser;
- session start/end and executed-operation audit;
- policy-controlled copy/upload/download and network egress;
- separate break-glass permission from safe monitoring.
The first product slice may register and monitor Hosts without exposing console. That is a complete safe increment, not a placeholder for arbitrary shell.
## Foundry projection
Foundry receives provider-neutral Data Products and semantic bindings:
- inventory/table: Host, endpoints, deployments, service instances;
- topology/graph: Host ↔ Deployment ↔ Service Instance ↔ Edge ↔ routes/devices;
- Inspector: current observations, configuration revision and provenance;
- map: only subjects with actual spatial observation or declared fixed location.
A VPS with no real coordinates does not receive invented map coordinates.
## Acceptance
- Host, endpoint, deployment, service instance and Edge registration use separate ids.
- Credentials are opaque references and do not enter Ontology, Data Products or browser state.
- Monitoring values carry source/time/freshness and do not mutate Host identity.
- Console, if implemented, is privileged, short-lived and audited.
- VPS map presentation is evidence-driven, never hardcoded.
@@ -0,0 +1,116 @@
# Observation Domain Ontology v0.1
Date: 2026-08-22
Status: product-required / source-evidenced baseline for Device Core telemetry, health and spatial projection.
## Purpose
Observation is the provider-neutral bridge from runtime evidence to Data Products, Foundry state facets and monitoring. It separates the subject from a time-qualified statement about that subject.
The package covers:
- generic observations;
- declared observed-property value contracts;
- source/mapping provenance;
- consumer-owned freshness policy;
- health observations;
- position observations.
It is deliberately separate from lifecycle events, desired configuration and commands.
## Canonical observation envelope
An owning runtime service publishes at least:
```text
observation_id
subject_id
subject_entity_id
property_id
value + value_type + optional unit
observed_at
received_at
source_id + mapping_version
quality/evidence metadata
```
Freshness policy is referenced by the consuming Data Product and includes TTL, grace and missing-value behavior. It is not silently embedded in the source value.
`observed_at` is when the described state existed. `received_at` is when NODE.DC accepted the evidence. Both are required where a source can delay or replay data.
## Properties and values
`observation.observed_property` defines exact semantics and allowed values. Provider field names are mapping inputs, not canonical property ids. A Data Product may expose only mapped properties and exact values declared by its contract.
Examples:
- host reachability;
- service readiness;
- Edge channel connectivity;
- device transport session state;
- latitude/longitude position;
- speed or battery measurement;
- provider-specific Gelios `signal_state` and `movement_state` retained as their existing closed value contracts.
Foundry profiles may style declared state facets. They may not invent `unknown`, `stale`, `offline` or aggregate health values unless the bound property contract defines them.
## Missing and stale evidence
Missing evidence means no acceptable observation is available. Expired evidence means an observation failed the selected freshness policy. Neither automatically means unhealthy.
If a product wants `unknown`, `unavailable` or `unhealthy`, the owning property contract must define the value and derivation rule. Derived values must retain source observations and mapping version.
This rule prevents three common conflations:
- source value vs consumer freshness;
- missing observation vs negative observation;
- health of Host vs health of Service/Edge/Device.
## Health observations
`observation.health_observation` targets one stable layer at a time:
- `infrastructure.host` for host reachability/resources;
- `infrastructure.service_instance` for service process/readiness;
- `device.edge_registration` for Core↔Edge channel acceptance;
- `device.device` for device-specific transport or service checks.
An aggregate status may be published only by a versioned derived-property contract that identifies its inputs, precedence and missing-data rules.
## Position observations
`observation.position_observation` includes coordinate reference, coordinates, observed time, source and quality. For direct B2 traffic the provenance chain is:
```text
asset.asset
◀─ active device.asset_binding ─ device.tracking_device
◀─ position observation source ─ device.session / adapter mapping
─▶ map.moving_object Data Product projection
```
The map subject remains the Asset. The B2 remains the source Device. The observation remains evidence. None becomes the renderer entity identity.
Infrastructure may use the same position contract for a real fixed or mobile location. An address label or datacenter name without resolved spatial evidence is insufficient.
## Provider mappings
Existing Gelios mappings remain explicit:
- `gelios.position_fix` can map to a position observation;
- `gelios.telemetry_snapshot` can produce generic observations only through a versioned property mapping;
- raw provider payloads remain restricted;
- direct B2 and Gelios observations retain distinct sources and can be reconciled without silent overwrite.
## Storage and retention
Ontology Core stores no runtime observations. Device Core, an External Data Plane or another owning telemetry service owns current state, history, retention, cursoring and replay. Data Products expose approved projections; Foundry is not a telemetry database.
## Acceptance
- Every observation names stable subject, property, source, observed time and received time.
- Freshness is explicit and separate from the source value.
- Missing evidence is not silently converted to unhealthy.
- Health checks target one infrastructure/device layer at a time.
- Position projection retains Asset, Device and Observation as separate subjects.
- Provider values reach Foundry only through versioned mappings.
@@ -0,0 +1,99 @@
# Party and Asset Domain Ontology v0.1
Date: 2026-08-22
Status: product-required / source-evidenced baseline for Device Core and Foundry composition.
## Purpose
This domain defines stable business actors and assets independently from application accounts, trackers, providers and renderers. It is intentionally a shallow graph. A subject acquires meaning through explicit relations and time-qualified assignments rather than through a deep inheritance tree.
The first accepted product case is Robot2B:
- a trike is `asset.asset`;
- B2 is a separate `device.tracking_device`;
- the active attachment is `device.asset_binding` with `valid_from`, optional `valid_to`, lifecycle state and provenance;
- a driver is `party.person` participating through a time-qualified `party.driver_role` and `asset.assignment`;
- IMEI remains `device.restricted_identifier` on the tracker;
- map presentation is a projection of the Asset through `map.moving_object`, backed by `observation.position_observation`.
Changing the tracker, driver, provider or renderer must not replace the trike identity.
## Authority boundary
Ontology Core owns canonical meanings, relation names, aliases, blocked conflations and evidence routing. It stores no party rows, PII, asset rows, assignments, positions or access decisions.
Owning products remain authoritative for runtime state:
- HUB/NDCAuth own users, memberships, identities and authentication;
- Device Core owns asset/device composition used by Device operations;
- business applications own people, organizations and role assignments in their contour;
- Data Products expose approved projections;
- Foundry renders those projections and does not create domain identity.
## Canonical graph
```text
party.person ──is_party──────────────▶ party.party
party.organization ──is_party───────▶ party.party
party.account ──represents_party────▶ party.party
party.driver_role ──assigned_to─────▶ party.person
party.driver_role ──operates────────▶ asset.asset
asset.asset ──classified_by─────────▶ asset.asset_type
asset.asset ──has_assignment────────▶ asset.assignment
asset.asset ──has_external_ref──────▶ asset.external_reference
asset.asset ──has_lifecycle_event───▶ asset.lifecycle_event
asset.asset ──projected_as──────────▶ map.moving_object
```
The relation record or owning runtime binding carries provenance and validity. A UI may show the current effective relation, but history must remain queryable.
## Identity rules
`asset.asset` uses a stable NODE.DC-owned identifier. External references are namespaced and replaceable. The following values may be indexed for reconciliation but may not become the Asset identity:
- IMEI or hardware serial;
- current tracker id;
- Gelios unit id;
- renderer entity id;
- current driver or owner id;
- project or collection id;
- display name.
`party.person`, `hub.user`, `ndcauth.identity`, `party.account` and `agent.identity` are separate source records. Their composition requires explicit bindings with source authority; matching display name or email alone is insufficient.
## Temporal assignments
Assignments use half-open validity semantics: `valid_from <= t < valid_to`; `valid_to = null` means the relation remains current. The owning service must prevent overlapping active assignments where the product contract requires exclusivity. Closing an assignment appends or transitions evidence; it does not erase the old relation.
Examples include:
- driver operates trike;
- organization owns/manages asset;
- asset belongs to an operational project;
- tracker is attached to asset;
- contact point is valid for a party.
## Foundry projection
Foundry consumes stable Data Product keys and semantic bindings. It may combine identity, technical profile, contacts, current position and state facets by stable subject id. It must not infer Party or Asset identity from a provider-specific payload.
An Asset can be shown on a map only when a bound Data Product supplies a valid position observation. Without spatial evidence, the same Asset remains available in table, graph or Inspector views.
## Gelios coexistence
Existing Gelios records remain provider evidence:
- `gelios.unit` can map to an Asset only through an explicit source binding;
- `gelios.tracker_device` can map to a Tracking Device only through an explicit device binding;
- these mappings do not make provider ids canonical;
- new B2 onboarding does not require Gelios.
## Acceptance
- Trike and B2 resolve to separate canonical subjects.
- Replacing B2 closes one binding and opens another without changing the Asset id.
- IMEI is restricted and absent from public Foundry projections unless an explicit field policy grants it.
- Driver is a temporal role, not a Person subtype or free-text Asset field.
- Map identity is the Asset/Data Product key, not a provider or renderer key.
@@ -31,3 +31,12 @@ Current working evidence artifacts:
- `EVIDENCE_LEDGER_ENGINE_DIRTY_BOUNDARY_P1.md`
- `EVIDENCE_LEDGER_ENGINE_WORKFLOW_P1.md`
- `ONTOLOGY_SERVICE_PLACEMENT_AND_USE_CASES.md`
Accepted additive domain-package baselines from 2026-08-22:
- `../PARTY_ASSET_DOMAIN_ONTOLOGY.md`
- `../DEVICE_DOMAIN_ONTOLOGY.md`
- `../INFRASTRUCTURE_DOMAIN_ONTOLOGY.md`
- `../OBSERVATION_DOMAIN_ONTOLOGY.md`
These packages keep Party, Asset, Device, Observation and infrastructure subjects in a shallow explicit graph. Runtime rows, telemetry, credentials, assignments and commands remain owned by their application services and are not stored in Ontology Core.
@@ -0,0 +1,38 @@
{
"fixture": "device-direct-b2-composition-v1",
"catalogVersion": "ontology-core-v0.4-pre+device-domain-0.1.0",
"project": { "entityId": "device.project", "sourceId": "device-project:robot2b-moscow" },
"asset": { "entityId": "asset.asset", "sourceId": "asset:trike-001", "assetType": "trike" },
"device": {
"entityId": "device.tracking_device",
"sourceId": "device:b2-001",
"restrictedIdentifiers": [
{ "entityId": "device.restricted_identifier", "namespace": "imei", "valueExposure": "restricted" }
],
"modelProfile": { "entityId": "device.model_profile", "sourceId": "model-profile:arnavi-b2-v1" }
},
"binding": {
"entityId": "device.asset_binding",
"sourceId": "device-asset-binding:b2-001:trike-001:20260822",
"deviceSourceId": "device:b2-001",
"assetSourceId": "asset:trike-001",
"validFrom": "2026-08-22T00:00:00Z",
"validTo": null,
"provenance": "device-core-direct-enrollment"
},
"positionObservation": {
"entityId": "observation.position_observation",
"subjectSourceId": "asset:trike-001",
"sourceDeviceId": "device:b2-001",
"observedAt": "2026-08-22T10:00:00Z",
"receivedAt": "2026-08-22T10:00:01Z",
"projectionEntityId": "map.moving_object"
},
"assertions": [
"asset.sourceId != device.sourceId",
"imei is not an asset identifier",
"binding is temporal and replaceable",
"map subject is the asset",
"Gelios connection is not required"
]
}
@@ -0,0 +1,30 @@
{
"fixture": "infrastructure-edge-host-composition-v1",
"catalogVersion": "ontology-core-v0.4-pre+infrastructure-domain-0.1.0",
"host": { "entityId": "infrastructure.host", "sourceId": "host:robot2b-edge-01", "kind": "vps" },
"endpoint": { "entityId": "infrastructure.endpoint", "sourceId": "endpoint:robot2b-edge-01:https", "visibility": "core-egress-only" },
"deployment": { "entityId": "infrastructure.deployment", "sourceId": "deployment:device-edge:20260822-001" },
"serviceInstance": { "entityId": "infrastructure.service_instance", "sourceId": "service-instance:device-edge:robot2b-edge-01" },
"edgeRegistration": { "entityId": "device.edge_registration", "sourceId": "edge-registration:robot2b-edge-01" },
"healthObservation": {
"entityId": "observation.health_observation",
"subjectSourceId": "service-instance:device-edge:robot2b-edge-01",
"propertyId": "service.readiness",
"observedAt": "2026-08-22T10:00:00Z",
"receivedAt": "2026-08-22T10:00:00Z",
"freshnessPolicyId": "health:service-readiness:60s"
},
"management": {
"credentialEntityId": "infrastructure.management_credential_reference",
"secretExposure": "opaque-reference-only",
"consoleEntityId": "infrastructure.management_session",
"mode": "privileged-break-glass"
},
"assertions": [
"host, endpoint, deployment, service instance and edge registration use distinct ids",
"integration.connection is not a host",
"missing health evidence is not automatically unhealthy",
"no management secret reaches the browser",
"map projection requires real spatial evidence"
]
}
+1
View File
@@ -18,6 +18,7 @@
"smoke:assistant-executor": "node src/assistant-action-executor.mjs --smoke",
"smoke:assistant-actions": "node src/assistant-action-resolver.mjs --smoke",
"smoke:assistant-policy": "node src/assistant-policy.mjs --smoke",
"smoke:device-domain": "node src/scripts/smoke-device-domain.mjs",
"smoke:mcp": "node src/scripts/smoke-mcp.mjs",
"smoke:resolver": "node src/resolver.mjs --smoke"
}
@@ -0,0 +1,104 @@
#!/usr/bin/env node
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import { loadCatalog, serviceRoot } from '../catalog.mjs'
const catalog = await loadCatalog()
const directFixture = JSON.parse(
await fs.readFile(path.join(serviceRoot, 'examples/device-direct-b2-composition.fixture.json'), 'utf8'),
)
const infrastructureFixture = JSON.parse(
await fs.readFile(path.join(serviceRoot, 'examples/infrastructure-edge-host-composition.fixture.json'), 'utf8'),
)
const requiredEntities = [
directFixture.project.entityId,
directFixture.asset.entityId,
directFixture.device.entityId,
directFixture.device.restrictedIdentifiers[0].entityId,
directFixture.device.modelProfile.entityId,
directFixture.binding.entityId,
directFixture.positionObservation.entityId,
directFixture.positionObservation.projectionEntityId,
infrastructureFixture.host.entityId,
infrastructureFixture.endpoint.entityId,
infrastructureFixture.deployment.entityId,
infrastructureFixture.serviceInstance.entityId,
infrastructureFixture.edgeRegistration.entityId,
infrastructureFixture.healthObservation.entityId,
infrastructureFixture.management.credentialEntityId,
infrastructureFixture.management.consoleEntityId,
]
for (const entityId of requiredEntities) {
assert.equal(catalog.entityById.has(entityId), true, `fixture entity missing from catalog: ${entityId}`)
}
for (const relationId of [
'device.tracking_device.tracks_asset',
'device.asset_binding.attaches_device',
'device.asset_binding.attaches_asset',
'asset.asset.is_map_moving_object',
'observation.position_observation.positions_asset',
'infrastructure.host.exposes_endpoint',
'infrastructure.deployment.targets_host',
'infrastructure.service_instance.runs_on_host',
'infrastructure.service_instance.realizes_deployment',
'device.edge_registration.runs_as_service_instance',
'observation.health_observation.targets_infrastructure',
'gelios.unit.maps_to_asset',
'gelios.tracker_device.maps_to_tracking_device',
'gelios.position_fix.maps_to_position_observation',
]) {
assert.equal(catalog.relationById.has(relationId), true, `required relation missing: ${relationId}`)
}
assert.notEqual(directFixture.asset.sourceId, directFixture.device.sourceId)
assert.equal(directFixture.binding.assetSourceId, directFixture.asset.sourceId)
assert.equal(directFixture.binding.deviceSourceId, directFixture.device.sourceId)
assert.equal(directFixture.binding.validTo, null)
assert.equal(directFixture.device.restrictedIdentifiers[0].valueExposure, 'restricted')
assert.equal(JSON.stringify(directFixture).includes('integration.connection'), false)
const infrastructureIds = [
infrastructureFixture.host.sourceId,
infrastructureFixture.endpoint.sourceId,
infrastructureFixture.deployment.sourceId,
infrastructureFixture.serviceInstance.sourceId,
infrastructureFixture.edgeRegistration.sourceId,
]
assert.equal(new Set(infrastructureIds).size, infrastructureIds.length)
assert.equal(infrastructureFixture.management.secretExposure, 'opaque-reference-only')
assert.equal(infrastructureFixture.management.mode, 'privileged-break-glass')
for (const guardrailId of [
'guardrail.asset.identity_survives_device_changes',
'guardrail.device.identifiers_are_restricted_not_identity',
'guardrail.device.asset_attachment_is_temporal',
'guardrail.infrastructure.host_is_not_connection',
'guardrail.infrastructure.management_is_break_glass',
'guardrail.observation.missing_is_not_automatically_unhealthy',
'guardrail.gelios.neutral_composition_requires_explicit_binding',
]) {
assert.equal(
catalog.guardrails.rules.some((rule) => rule.id === guardrailId),
true,
`required guardrail missing: ${guardrailId}`,
)
}
console.log(JSON.stringify({
ok: true,
checks: [
'direct_b2_and_trike_have_distinct_identities',
'device_asset_binding_is_temporal',
'imei_exposure_is_restricted',
'direct_fixture_has_no_gelios_dependency',
'host_runtime_layers_have_distinct_identities',
'management_secret_is_opaque_reference_only',
'neutral_gelios_bridge_relations_exist',
'composition_guardrails_exist',
],
}, null, 2))
@@ -66,6 +66,51 @@ try {
})
assert.equal(guardrails.result.structuredContent.rules.some((rule) => rule.id === 'guardrail.gelios.commands_are_red_domain'), true)
const directTracker = await rpc(baseUrl, TOKEN, 51, 'tools/call', {
name: 'ontology_get_entity',
arguments: { term: 'B2 tracker' },
})
assert.equal(directTracker.result.structuredContent.entity.id, 'device.tracking_device')
const infrastructureHost = await rpc(baseUrl, TOKEN, 52, 'tools/call', {
name: 'ontology_get_entity',
arguments: { term: 'внешний VPS' },
})
assert.equal(infrastructureHost.result.structuredContent.entity.id, 'infrastructure.host')
const restrictedIdentifierGuardrails = await rpc(baseUrl, TOKEN, 53, 'tools/call', {
name: 'ontology_get_guardrails',
arguments: { entityId: 'device.restricted_identifier' },
})
assert.equal(
restrictedIdentifierGuardrails.result.structuredContent.rules.some(
(rule) => rule.id === 'guardrail.device.identifiers_are_restricted_not_identity',
),
true,
)
const hostGuardrails = await rpc(baseUrl, TOKEN, 54, 'tools/call', {
name: 'ontology_get_guardrails',
arguments: { entityId: 'infrastructure.host' },
})
assert.equal(
hostGuardrails.result.structuredContent.rules.some(
(rule) => rule.id === 'guardrail.infrastructure.host_is_not_connection',
),
true,
)
const positionObservation = await rpc(baseUrl, TOKEN, 55, 'tools/call', {
name: 'ontology_get_entity',
arguments: { entityId: 'observation.position_observation' },
})
assert.equal(
positionObservation.result.structuredContent.relations.some(
(relation) => relation.id === 'gelios.position_fix.maps_to_position_observation',
),
true,
)
const unauthorized = await fetch(`${baseUrl}/mcp`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
@@ -82,6 +127,11 @@ try {
'gelios_alias_resolution',
'gelios_value_contract_visible',
'gelios_command_guardrail_visible',
'direct_b2_tracker_alias_resolution',
'infrastructure_host_alias_resolution',
'restricted_identifier_guardrail_visible',
'host_connection_conflation_blocked',
'gelios_position_neutral_mapping_visible',
'evidence_paths_not_exposed',
'internal_bearer_required',
],