@amalgm/shell 0.1.143 → 0.1.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
3
3
  import { open, readFile } from "node:fs/promises";
4
4
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
6
- import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, containedRecords, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, materializedSlotNames, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } from "@amalgm/live";
6
+ import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, containedRecords, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, materializedSlotNames, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, portableFileMode, GRAPH_HEAD_OPERATION_KIND, } from "@amalgm/live";
7
7
  import Database from "better-sqlite3";
8
8
  import { atomicCopy, atomicWrite, ensurePrivateDir, ensureUserDir } from "./filesystem.js";
9
9
  import { emitFilesStage, } from "./files-observability.js";
@@ -945,7 +945,7 @@ export class UserGroundHost {
945
945
  // seal inventory; an older gateway cannot say which sealed artifacts are
946
946
  // packed, and this shell plans pack-span downloads and repacks against
947
947
  // exactly that answer.
948
- if (this.wire.protocolVersion < 16) {
948
+ if (this.wire.protocolVersion < 17) {
949
949
  throw new Error("gateway speaks an older wire protocol than this shell");
950
950
  }
951
951
  this.adoptCloudDeliveryBaseline(identity, this.cloudState.deliveryWatermark);
@@ -2614,7 +2614,7 @@ export class UserGroundHost {
2614
2614
  for (const frame of answers) {
2615
2615
  if (!Array.isArray(frame.sealed) || !Array.isArray(frame.packed)) {
2616
2616
  throw new Error("content authority answered a seal inventory without "
2617
- + "placement; protocol 16 requires the gateway to deploy before this machine");
2617
+ + "placement; protocol 17 requires the gateway to deploy before this machine");
2618
2618
  }
2619
2619
  for (const hash of frame.sealed)
2620
2620
  sealed.add(String(hash));
@@ -3998,6 +3998,7 @@ function portableRecord(row) {
3998
3998
  status: row.status,
3999
3999
  version: row.version,
4000
4000
  payloadVersion: derived ? null : row.payloadVersion,
4001
+ fileMode: portableFileMode(row.type, row.filesystemMode),
4001
4002
  transportVersion: derived ? null : row.transportVersion,
4002
4003
  };
4003
4004
  }
@@ -4021,11 +4022,18 @@ function coreGroundRecords(file, userEmail) {
4021
4022
  )
4022
4023
  SELECT uuid, type, parent_uuid AS parentUUID, name, status, version,
4023
4024
  payload_version AS payloadVersion,
4025
+ filesystem_mode AS filesystemMode,
4024
4026
  transport_version AS transportVersion
4025
4027
  FROM entities WHERE uuid IN (SELECT uuid FROM core) ORDER BY uuid
4026
- `).all(userEmail).map((record) => (record.type === "workspace" || record.type === "folder"
4027
- ? { ...record, payloadVersion: null, transportVersion: null }
4028
- : record));
4028
+ `).all(userEmail).map(({ filesystemMode, ...record }) => {
4029
+ const derived = record.type === "workspace" || record.type === "folder";
4030
+ return {
4031
+ ...record,
4032
+ payloadVersion: derived ? null : record.payloadVersion,
4033
+ fileMode: portableFileMode(record.type, filesystemMode),
4034
+ transportVersion: derived ? null : record.transportVersion,
4035
+ };
4036
+ });
4029
4037
  }
4030
4038
  finally {
4031
4039
  database.close();
@@ -5112,6 +5120,7 @@ async function reconcileRoot(input) {
5112
5120
  const existingVersionByUuid = new Map(existingRows.map((row) => [row.uuid, row]));
5113
5121
  const rowsFromEntries = () => entries.map((entry) => {
5114
5122
  const container = entry.type === "workspace" || entry.type === "folder";
5123
+ const fileMode = portableFileMode(entry.type, entry.filesystemMode);
5115
5124
  const stored = container ? undefined : existingVersionByUuid.get(entry.uuid);
5116
5125
  const storedVersion = stored !== undefined
5117
5126
  && stored.type === entry.type
@@ -5119,6 +5128,7 @@ async function reconcileRoot(input) {
5119
5128
  && stored.name === entry.name
5120
5129
  && stored.status === "active"
5121
5130
  && stored.payloadVersion === entry.payloadVersion
5131
+ && portableFileMode(stored.type, stored.filesystemMode) === fileMode
5122
5132
  ? stored.version
5123
5133
  : null;
5124
5134
  const record = {
@@ -5135,8 +5145,10 @@ async function reconcileRoot(input) {
5135
5145
  payloadVersion: container
5136
5146
  ? membershipHash(children.get(entry.uuid) ?? [], sha256Hex)
5137
5147
  : entry.payloadVersion,
5148
+ fileMode,
5138
5149
  }, sha256Hex),
5139
5150
  payloadVersion: entry.payloadVersion,
5151
+ fileMode,
5140
5152
  transportVersion: entry.transportVersion,
5141
5153
  };
5142
5154
  return {
@@ -5880,9 +5892,12 @@ async function materializeRecords(input) {
5880
5892
  // transport is never its final worktree. Ordinary file bytes are
5881
5893
  // the cold Add's mass, and they land exactly once.
5882
5894
  if (stagedDestinations && record.type !== "link") {
5895
+ if (record.fileMode === null) {
5896
+ throw new Error(`cloud ordinary file ${record.uuid} has no portable mode`);
5897
+ }
5883
5898
  const destination = destinationOf(paths.get(record.uuid));
5884
5899
  const list = stagedDestinations.get(artifact.contentHash) ?? [];
5885
- list.push(destination);
5900
+ list.push({ path: destination, fileMode: record.fileMode });
5886
5901
  stagedDestinations.set(artifact.contentHash, list);
5887
5902
  }
5888
5903
  }
@@ -5890,8 +5905,9 @@ async function materializeRecords(input) {
5890
5905
  if (stagedDestinations) {
5891
5906
  // A deterministic primary destination keeps resume evidence valid: the
5892
5907
  // same first path receives the stream on every attempt.
5893
- for (const list of stagedDestinations.values())
5894
- list.sort();
5908
+ for (const list of stagedDestinations.values()) {
5909
+ list.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
5910
+ }
5895
5911
  }
5896
5912
  const contentStarted = performance.now();
5897
5913
  const fileContent = await readContent([...wanted.values()], staged && stagedDestinations
@@ -6095,9 +6111,7 @@ async function materializeRecords(input) {
6095
6111
  throw new Error(`cloud materialization conflicts with ${relativePath}`);
6096
6112
  }
6097
6113
  ensureUserDir(dirname(destination));
6098
- // Mode is not portable state: a fresh user file gets 0644, the same
6099
- // default Apply gives a fresh target, never the cache's private 0600.
6100
- atomicCopy(local.file, destination, 0o644);
6114
+ atomicCopy(local.file, destination, record.fileMode);
6101
6115
  }
6102
6116
  }
6103
6117
  else {