From 4957e7aaeb78de5203eab768d7ee0f2b394132b8 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 6 Aug 2026 10:53:52 +0800 Subject: [PATCH 1/6] Add HEAD and ETag to storage service --- .dockerignore | 1 + .gitignore | 1 + .mise/config.js.toml | 8 + generated/dart-rest/lib/clients/storage.dart | 15 + .../dart-rest/lib/clients/storage.g.dart | 22 + generated/dart-rest/lib/rest_client.dart | 5 +- .../et_rest_client/api/storage/head_file.py | 121 ++++ generated/rust-rest/src/lib.rs | 63 +- generated/specs/rest.yaml | 59 +- generated/zig-rest/src/et_rest_client.zig | 25 + pnpm-lock.yaml | 582 ++++++++++++++++++ pnpm-workspace.yaml | 8 + services/storage/src/lib.rs | 11 +- services/storage/src/routes.rs | 82 ++- services/storage/tests/put.rs | 84 ++- services/ws-modules/js-data1/README.md | 41 ++ services/ws-modules/js-data1/build.mjs | 17 + services/ws-modules/js-data1/package.json | 17 + services/ws-modules/js-data1/pkg/package.json | 8 + services/ws-modules/js-data1/src/index.js | 145 +++++ services/ws-web-runner/tests/modules.rs | 20 + utilities/int-gen/src/openapi.rs | 6 +- 22 files changed, 1325 insertions(+), 16 deletions(-) create mode 100644 generated/python-rest/et_rest_client/api/storage/head_file.py create mode 100644 services/ws-modules/js-data1/README.md create mode 100644 services/ws-modules/js-data1/build.mjs create mode 100644 services/ws-modules/js-data1/package.json create mode 100644 services/ws-modules/js-data1/pkg/package.json create mode 100644 services/ws-modules/js-data1/src/index.js diff --git a/.dockerignore b/.dockerignore index a3737975..7c123aaa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,6 +13,7 @@ services/ws-wasm-agent/pkg/ services/ws-modules/pywasm1/pkg/ services/ws-modules/rdata1/pkg/webr/ services/ws-modules/rcomm1/pkg/webr/ +services/ws-modules/js-data1/pkg/et_ws_js_data1.js services/ws-server/static/models/ **/.zig-cache/ **/zig-out/ diff --git a/.gitignore b/.gitignore index 28020409..f12d2a40 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ services/ws-wasm-agent/pkg/ services/ws-modules/pywasm1/pkg/ services/ws-modules/rdata1/pkg/webr/ services/ws-modules/rcomm1/pkg/webr/ +services/ws-modules/js-data1/pkg/et_ws_js_data1.js services/ws-server/static/models/ .zig-cache/ zig-out/ diff --git a/.mise/config.js.toml b/.mise/config.js.toml index b2fb4d59..e205d900 100644 --- a/.mise/config.js.toml +++ b/.mise/config.js.toml @@ -80,6 +80,14 @@ description = "Build the face detection workflow WASM module" dir = "services/ws-modules/face-detection" run = "{{ vars.web_pack_cov }} {{ vars.no_opt }}{{ vars.web_cov_feat }} && {{ vars.et_cli }} module-package-json" +[tasks.build-ws-js-data1-module] +description = "Bundle the js-data1 module (AWS SDK for JS v3) into pkg/ with esbuild" +dir = "services/ws-modules/js-data1" +# esbuild bundles @aws-sdk/client-s3 into one self-contained ESM. +# Its @aws-sdk/* + @smithy/* graph uses bare-specifier imports that don't resolve as served static files, so +# bundling is required; pnpm install brings the workspace deps in first. +run = "pnpm install && pnpm run build" + [tasks.oxlint-check] description = "Lint JavaScript/TypeScript sources (oxlint)" # oxlint walks the working tree from CWD, honouring .gitignore; no path arg needed. diff --git a/generated/dart-rest/lib/clients/storage.dart b/generated/dart-rest/lib/clients/storage.dart index b2bb5585..845c5259 100644 --- a/generated/dart-rest/lib/clients/storage.dart +++ b/generated/dart-rest/lib/clients/storage.dart @@ -45,4 +45,19 @@ abstract class Storage { @Path('filename') required String filename, @Body() required Uint8List body, }); + + /// Return a stored object's metadata without its body (S3 `HeadObject`). + /// + /// Same addressing and 404 handling as [`get_file`], but the response carries headers only: the object's `ETag`. + /// and its size as `Content-Length`. S3 clients issue `HEAD` to stat an object (existence, size, entity tag). + /// before downloading, so it reports the same `ETag` a `GET` would. + /// + /// [agentId] - Agent identifier. + /// + /// [filename] - Stored filename. + @HEAD('/storage/{agent_id}/{filename}') + Future headFile({ + @Path('agent_id') required String agentId, + @Path('filename') required String filename, + }); } diff --git a/generated/dart-rest/lib/clients/storage.g.dart b/generated/dart-rest/lib/clients/storage.g.dart index 9834d37b..013d7859 100644 --- a/generated/dart-rest/lib/clients/storage.g.dart +++ b/generated/dart-rest/lib/clients/storage.g.dart @@ -77,6 +77,28 @@ class _Storage implements Storage { await _dio.fetch(_options); } + @override + Future headFile({ + required String agentId, + required String filename, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'HEAD', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/storage/${agentId}/${filename}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/generated/dart-rest/lib/rest_client.dart b/generated/dart-rest/lib/rest_client.dart index 4e9665af..7723553d 100644 --- a/generated/dart-rest/lib/rest_client.dart +++ b/generated/dart-rest/lib/rest_client.dart @@ -10,7 +10,10 @@ import 'clients/storage.dart'; /// Edge Toolkit REST API `v0.1.0`. /// -/// ws-server HTTP surface: health probe, module discovery, module assets, per-agent storage. +/// ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. +/// The storage routes are an anonymous S3-compatible interface -- addressed path-style as. +/// /storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a. +/// standard S3 client can read and write objects without credentials. class RestClient { RestClient(Dio dio, {String? baseUrl}) : _dio = dio, _baseUrl = baseUrl; diff --git a/generated/python-rest/et_rest_client/api/storage/head_file.py b/generated/python-rest/et_rest_client/api/storage/head_file.py new file mode 100644 index 00000000..aecd3a46 --- /dev/null +++ b/generated/python-rest/et_rest_client/api/storage/head_file.py @@ -0,0 +1,121 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs( + agent_id: str, + filename: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "head", + "url": "/storage/{agent_id}/{filename}".format( + agent_id=quote(str(agent_id), safe=""), + filename=quote(str(filename), safe=""), + ), + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: + if response.status_code == 200: + return None + + if response.status_code == 404: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + agent_id: str, + filename: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """Return a stored object's metadata without its body (S3 `HeadObject`). + + Same addressing and 404 handling as [`get_file`], but the response carries headers only: the + object's `ETag` + and its size as `Content-Length`. S3 clients issue `HEAD` to stat an object (existence, size, entity + tag) + before downloading, so it reports the same `ETag` a `GET` would. + + Args: + agent_id (str): + filename (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + agent_id=agent_id, + filename=filename, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + agent_id: str, + filename: str, + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """Return a stored object's metadata without its body (S3 `HeadObject`). + + Same addressing and 404 handling as [`get_file`], but the response carries headers only: the + object's `ETag` + and its size as `Content-Length`. S3 clients issue `HEAD` to stat an object (existence, size, entity + tag) + before downloading, so it reports the same `ETag` a `GET` would. + + Args: + agent_id (str): + filename (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + agent_id=agent_id, + filename=filename, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/generated/rust-rest/src/lib.rs b/generated/rust-rest/src/lib.rs index 67a730c1..d6f1170f 100644 --- a/generated/rust-rest/src/lib.rs +++ b/generated/rust-rest/src/lib.rs @@ -66,7 +66,10 @@ pub mod types { #[derive(Clone, Debug)] /**Client for Edge Toolkit REST API -ws-server HTTP surface: health probe, module discovery, module assets, per-agent storage. +ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. +The storage routes are an anonymous S3-compatible interface -- addressed path-style as +/storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a +standard S3 client can read and write objects without credentials. Version: 0.1.0*/ pub struct Client { @@ -467,6 +470,64 @@ impl Client { _ => Err(Error::UnexpectedResponse(response)), } } + /**Return a stored object's metadata without its body (S3 `HeadObject`) + + Same addressing and 404 handling as [`get_file`], but the response carries headers only: the object's `ETag` + and its size as `Content-Length`. S3 clients issue `HEAD` to stat an object (existence, size, entity tag) + before downloading, so it reports the same `ETag` a `GET` would. + + Sends a `HEAD` request to `/storage/{agent_id}/{filename}` + + Arguments: + - `agent_id`: Agent identifier + - `filename`: Stored filename + */ + pub async fn head_file<'a>(&'a self, agent_id: &'a str, filename: &'a str) -> Result, Error<()>> { + let url = format!( + "{}/storage/{}/{}", + self.baseurl, + encode_path(&agent_id.to_string()), + encode_path(&filename.to_string()), + ); + let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize); + header_map.append( + ::reqwest::header::HeaderName::from_static("api-version"), + ::reqwest::header::HeaderValue::from_static(Self::api_version()), + ); + #[allow(unused_mut)] + let mut request = self.client.head(url).headers(header_map).build()?; + let info = OperationInfo { + operation_id: "head_file", + }; + match (|request: &mut ::reqwest::Request| { + #[cfg(feature = "tracing")] + { + let cx = <::tracing::Span as ::tracing_opentelemetry::OpenTelemetrySpanExt>::context( + &::tracing::Span::current(), + ); + ::opentelemetry::global::get_text_map_propagator(|propagator| { + propagator.inject_context(&cx, &mut ::opentelemetry_http::HeaderInjector(request.headers_mut())); + }); + } + #[cfg(not(feature = "tracing"))] + let _ = request; + async { Ok::<(), ::std::convert::Infallible>(()) } + })(&mut request) + .await + { + Ok(_) => {} + Err(e) => return Err(Error::Custom(e.to_string())), + } + self.pre(&mut request, &info).await?; + let result = self.exec(request, &info).await; + self.post(&result, &info).await?; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + 404u16 => Err(Error::ErrorResponse(ResponseValue::empty(response))), + _ => Err(Error::UnexpectedResponse(response)), + } + } } /// Items consumers will typically use such as the Client. pub mod prelude { diff --git a/generated/specs/rest.yaml b/generated/specs/rest.yaml index 9acb2771..023205c0 100644 --- a/generated/specs/rest.yaml +++ b/generated/specs/rest.yaml @@ -1,7 +1,11 @@ openapi: 3.0.3 info: title: Edge Toolkit REST API - description: "ws-server HTTP surface: health probe, module discovery, module assets, per-agent storage." + description: |- + ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. + The storage routes are an anonymous S3-compatible interface -- addressed path-style as + /storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a + standard S3 client can read and write objects without credentials. version: 0.1.0 servers: - url: http://localhost:8080 @@ -93,6 +97,12 @@ paths: responses: "200": description: Stored file contents + headers: + ETag: + description: Entity tag of the stored object + style: simple + schema: + type: string content: application/octet-stream: {} "404": @@ -137,10 +147,57 @@ paths: responses: "200": description: File stored + headers: + ETag: + description: Entity tag of the stored object + style: simple + schema: + type: string "400": description: Invalid filename "404": description: Agent not found + head: + tags: + - storage + summary: Return a stored object's metadata without its body (S3 `HeadObject`). + description: |- + Same addressing and 404 handling as [`get_file`], but the response carries headers only: the object's `ETag` + and its size as `Content-Length`. S3 clients issue `HEAD` to stat an object (existence, size, entity tag) + before downloading, so it reports the same `ETag` a `GET` would. + operationId: head_file + parameters: + - in: path + name: agent_id + description: Agent identifier + required: true + schema: + type: string + style: simple + - in: path + name: filename + description: Stored filename + required: true + schema: + type: string + style: simple + responses: + "200": + description: Object metadata (no body) + headers: + Content-Length: + description: Size of the stored object in bytes + style: simple + schema: + type: integer + format: int64 + ETag: + description: Entity tag of the stored object + style: simple + schema: + type: string + "404": + description: No such file components: schemas: HealthResponse: diff --git a/generated/zig-rest/src/et_rest_client.zig b/generated/zig-rest/src/et_rest_client.zig index 6bac7eb2..f0c993f1 100644 --- a/generated/zig-rest/src/et_rest_client.zig +++ b/generated/zig-rest/src/et_rest_client.zig @@ -557,6 +557,31 @@ pub fn put_fileRaw(client: *Client, agent_id: []const u8, filename: []const u8, return requestRawWithContentType(client, std.http.Method.PUT, uri_buf.written(), payload, "application/octet-stream"); } +///////////////// +// Summary: +// Return a stored object's metadata without its body (S3 `HeadObject`). +// +// Description: +// Same addressing and 404 handling as [`get_file`], but the response carries headers only: the object's `ETag` +// and its size as `Content-Length`. S3 clients issue `HEAD` to stat an object (existence, size, entity tag) +// before downloading, so it reports the same `ETag` a `GET` would. +// +pub fn head_file(client: *Client, agent_id: []const u8, filename: []const u8) !void { + var raw = try head_fileRaw(client, agent_id, filename); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn head_fileRaw(client: *Client, agent_id: []const u8, filename: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/storage/{s}/{s}", .{ client.base_url, agent_id, filename }); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.HEAD, uri_buf.written(), payload); +} + ///////////////// // Summary: // Fetch a file from a module's bundled static assets. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c954183..ed27efbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,16 @@ importers: .: {} + services/ws-modules/js-data1: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3 + version: 3.1103.0 + devDependencies: + esbuild: + specifier: ^0.25 + version: 0.25.12 + services/ws-server/static: dependencies: et-ws-wasm-agent: @@ -22,6 +32,234 @@ importers: packages: + '@aws-sdk/checksums@3.1000.26': + resolution: {integrity: sha512-CGznePoL+1oWCSzqmkvlYMpWQEZohPR3LntjKXXfVH+oh6Kd8d+yHLkjpiAGwPIHAxFe4OAq3aKfRnv/wqWIyA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1103.0': + resolution: {integrity: sha512-FO7SB2vLhZRN3lBHVhMhRm3YKSHK8e917qjB8LMEEhU69cQ0Ahd/OMyezu+KqNANqEtyxfSnFVvYt81x6ZKGZA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.6': + resolution: {integrity: sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.67': + resolution: {integrity: sha512-rcIpk5kxUqDaaNa6Xk23pQ6ViY7jlqzmfFWCahQcBT97ddXaXYYwzCen9Tz1Jvo6aJft6wDl5bN44/Jw5B4oLA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.69': + resolution: {integrity: sha512-nggwJtZ4eeNsUw5IeWBMXsi1ryct5idi0K+/SCRF3kybLubOMaNTb3XCihXpWMiVpyzyPeIrl0zTkzhBH9porA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.12': + resolution: {integrity: sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.74': + resolution: {integrity: sha512-0AQfDcf99TNmqVKv0owHrw/TQs6i4ZE5t9qmz6NvO53bE/sA/tpXhXL9AAcEP1qHc6Zzjd1UMb69+/9zdhvY3g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.78': + resolution: {integrity: sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.67': + resolution: {integrity: sha512-IlUEejorGTWKb4/Dm7K5Yw4QxUmXLThLhrvBmzVBqZFTbW72cv9LTcITmo1dsnYriALE4h68mOq4LB99x6sQ7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.11': + resolution: {integrity: sha512-gAQBkBZxUB84d71+pPcI9L+jh2ujhuAVxc/4FgGiWFDjkPBlMKxzd5XDtkSXTFX8Ro7ansnT88+XadasxMeCRw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.73': + resolution: {integrity: sha512-SnlEmQa6SjOgs6iOPLUQl1Eyq4AKiAdPQlkOhFhqNfDtDCwibMGvL6QlkSmf3o6vAUSImzdPCxowT5dfQUZP1A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.72': + resolution: {integrity: sha512-lSAoVPvQxX1d8TOM6waKDBQrvvZcm4w6pCldFAsRUffEaXq6lYY0pPyew3KlLu6Xqb74DXI42hGvSsbGBLljlw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.41': + resolution: {integrity: sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.43': + resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1103.0': + resolution: {integrity: sha512-N4wy26MNn31ItGVHYHPrEuCIFY4MBBjC+C5v1lJKqIUSA7OZBdhleCY53zCCrXn27hsk7YNOaTuhQu807S4AfQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.37': + resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -49,9 +287,41 @@ packages: '@protobufjs/utf8@1.1.2': resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@smithy/core@3.31.1': + resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.16': + resolution: {integrity: sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.6.13': + resolution: {integrity: sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.9.13': + resolution: {integrity: sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.6.12': + resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + '@types/node@26.1.2': resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + flatbuffers@25.9.23: resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} @@ -82,11 +352,257 @@ packages: three: optional: true + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} snapshots: + '@aws-sdk/checksums@3.1000.26': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1103.0': + dependencies: + '@aws-sdk/checksums': 3.1000.26 + '@aws-sdk/core': 3.977.6 + '@aws-sdk/credential-provider-node': 3.972.78 + '@aws-sdk/middleware-sdk-s3': 3.972.72 + '@aws-sdk/signature-v4-multi-region': 3.996.43 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.6': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.37 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.31.1 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.67': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.12': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/credential-provider-env': 3.972.67 + '@aws-sdk/credential-provider-http': 3.972.69 + '@aws-sdk/credential-provider-login': 3.972.74 + '@aws-sdk/credential-provider-process': 3.972.67 + '@aws-sdk/credential-provider-sso': 3.973.11 + '@aws-sdk/credential-provider-web-identity': 3.972.73 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/credential-provider-imds': 4.4.16 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.74': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.78': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.67 + '@aws-sdk/credential-provider-http': 3.972.69 + '@aws-sdk/credential-provider-ini': 3.973.12 + '@aws-sdk/credential-provider-process': 3.972.67 + '@aws-sdk/credential-provider-sso': 3.973.11 + '@aws-sdk/credential-provider-web-identity': 3.972.73 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/credential-provider-imds': 4.4.16 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.67': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.11': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/token-providers': 3.1103.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.73': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/signature-v4-multi-region': 3.996.43 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.41': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/signature-v4-multi-region': 3.996.43 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.43': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1103.0': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.37': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -107,10 +623,74 @@ snapshots: '@protobufjs/utf8@1.1.2': {} + '@smithy/core@3.31.1': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.16': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.6.13': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.9.13': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.6.12': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + '@types/node@26.1.2': dependencies: undici-types: 8.3.0 + bowser@2.14.1: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + flatbuffers@25.9.23: {} guid-typescript@1.0.9: {} @@ -146,4 +726,6 @@ snapshots: stats-gl@4.2.3: {} + tslib@2.8.1: {} + undici-types@8.3.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cd334f8e..c9f444e6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,3 +7,11 @@ # Some of their deps are loaded via mise installed tools, which are checked separately. packages: - services/ws-server/static + # js-data1 is a real workspace member, not a served pkg/ dir. + # It declares published npm deps (@aws-sdk/client-s3, esbuild) that must land in the root lockfile for + # osv-scanner; its bundled output under pkg/ is generated, not a member. + - services/ws-modules/js-data1 +# esbuild ships its platform binary via a postinstall build script; allow it so the js-data1 bundle can build. +# Everything else stays blocked by pnpm's default supply-chain policy. +allowBuilds: + esbuild: true diff --git a/services/storage/src/lib.rs b/services/storage/src/lib.rs index c4ba7501..d266fd1e 100644 --- a/services/storage/src/lib.rs +++ b/services/storage/src/lib.rs @@ -1,7 +1,7 @@ //! Agent file storage, backed by any `object_store` backend. //! -//! The wire protocol is unchanged (`PUT`/`GET /storage/{agent_id}/{filename}`); only the storage layer beneath -//! it is pluggable. [`StorageConfig::url`] selects the backend and defaults to a `file://` URL under +//! The wire protocol (`PUT`/`GET`/`HEAD /storage/{agent_id}/{filename}`) is stable across backends; only the storage +//! layer beneath it is pluggable. [`StorageConfig::url`] selects the backend and defaults to a `file://` URL under //! [`default_storage_folder`], so nothing needs configuring for research use; an `object_store` URL such as //! `s3://bucket` points at a remote instead. Objects are addressed as `/` under whichever //! store is in use, so the local-disk layout is the same as before. @@ -20,7 +20,7 @@ use thiserror::Error; pub mod routes; mod tty_image; -pub use self::routes::{get_file, put_file}; +pub use self::routes::{get_file, head_file, put_file}; /// Default storage directory. #[must_use] @@ -152,7 +152,7 @@ pub fn build_store(config: &StorageConfig) -> Result Ok(Arc::from(store)) } -/// Register `PUT /storage/{agent_id}/{filename}` and `GET /storage/{agent_id}/{filename}`. +/// Register `PUT`, `GET` and `HEAD` on `/storage/{agent_id}/{filename}`. /// /// # Panics /// @@ -174,5 +174,6 @@ where let _configured = cfg .app_data(web::Data::::from(store)) .route("/storage/{agent_id}/{filename}", web::put().to(put_file::)) - .route("/storage/{agent_id}/{filename}", web::get().to(get_file)); + .route("/storage/{agent_id}/{filename}", web::get().to(get_file)) + .route("/storage/{agent_id}/{filename}", web::head().to(head_file)); } diff --git a/services/storage/src/routes.rs b/services/storage/src/routes.rs index 146db52f..e3171ed5 100644 --- a/services/storage/src/routes.rs +++ b/services/storage/src/routes.rs @@ -23,7 +23,8 @@ use std::path::PathBuf; -use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::http::header; +use actix_web::{HttpRequest, HttpResponse, HttpResponseBuilder, web}; use edge_toolkit::ws_server::AgentRegistry; use futures_util::StreamExt as _; use object_store::{ObjectStore, ObjectStoreExt as _, PutPayload}; @@ -56,6 +57,17 @@ fn agent_and_filename(req: &HttpRequest) -> Result<(String, PathBuf), StorageErr Ok((agent_id, filename)) } +/// Attach the object's `ETag` header to a response when the backend reported one. +/// +/// `object_store` surfaces an entity tag on writes (`PutResult`) and reads (`ObjectMeta`) for every backend that +/// has one -- the S3 MD5/opaque tag, or a size+mtime tag for local disk -- and `None` only when a backend cannot +/// produce one. Emitting it lets S3 clients (and HTTP conditional requests) observe the same tag the store holds. +fn insert_etag(response: &mut HttpResponseBuilder, e_tag: Option<&str>) { + if let Some(etag) = e_tag { + let _inserted = response.insert_header((header::ETAG, etag)); + } +} + /// Phantom type used to label binary request/response bodies as `string`/`binary`. /// /// Never constructed at runtime; only exists under the `openapi-spec` feature @@ -86,7 +98,9 @@ pub struct BinaryBlob(#[expect(dead_code)] Vec); description = "Raw file bytes" ), responses( - (status = 200, description = "File stored"), + (status = 200, description = "File stored", headers( + ("ETag" = String, description = "Entity tag of the stored object") + )), (status = 400, description = "Invalid filename"), (status = 404, description = "Agent not found") ) @@ -124,7 +138,7 @@ where let path = object_path(&agent_id, &filename); info!("Agent {} storing file: {}", agent_id, path); - let _put_result = store.put(&path, PutPayload::from(body.clone())).await?; + let put_result = store.put(&path, PutPayload::from(body.clone())).await?; // This handler is the only path a file reaches storage through, so it is where every stored file can be // watched exactly once with an accurate byte count and zero extra I/O -- a separate filesystem watcher @@ -134,7 +148,9 @@ where show_image_on_tty(&body); } - Ok(HttpResponse::Ok().finish()) + let mut response = HttpResponse::Ok(); + insert_etag(&mut response, put_result.e_tag.as_deref()); + Ok(response.finish()) } /// Render a thumbnail of the just-stored image bytes directly to stdout. @@ -183,7 +199,9 @@ pub fn is_image_filename(filename: &std::path::Path) -> bool { ("filename" = String, Path, description = "Stored filename") ), responses( - (status = 200, description = "Stored file contents", content_type = "application/octet-stream"), + (status = 200, description = "Stored file contents", content_type = "application/octet-stream", headers( + ("ETag" = String, description = "Entity tag of the stored object") + )), (status = 404, description = "No such file") ) ) @@ -203,7 +221,59 @@ pub async fn get_file(req: HttpRequest, store: web::Data) -> Re Err(object_store::Error::NotFound { .. }) => return Err(StorageError::ObjectNotFound), Err(error) => return Err(error.into()), }; + // Read the entity tag off the metadata before `bytes()` consumes the `GetResult`. + let e_tag = object.meta.e_tag.clone(); let body = object.bytes().await?; - Ok(HttpResponse::Ok().content_type("application/octet-stream").body(body)) + let mut response = HttpResponse::Ok(); + let _typed = response.content_type("application/octet-stream"); + insert_etag(&mut response, e_tag.as_deref()); + Ok(response.body(body)) +} + +/// Return a stored object's metadata without its body (S3 `HeadObject`). +/// +/// Same addressing and 404 handling as [`get_file`], but the response carries headers only: the object's `ETag` +/// and its size as `Content-Length`. S3 clients issue `HEAD` to stat an object (existence, size, entity tag) +/// before downloading, so it reports the same `ETag` a `GET` would. +#[cfg_attr( + feature = "openapi-spec", + utoipa::path( + head, + path = "/storage/{agent_id}/{filename}", + tag = "storage", + params( + ("agent_id" = String, Path, description = "Agent identifier"), + ("filename" = String, Path, description = "Stored filename") + ), + responses( + (status = 200, description = "Object metadata (no body)", headers( + ("ETag" = String, description = "Entity tag of the stored object"), + ("Content-Length" = i64, description = "Size of the stored object in bytes") + )), + (status = 404, description = "No such file") + ) + ) +)] +#[expect( + clippy::future_not_send, + reason = "actix-web HttpRequest is !Send by design; handler runs on actix's single-threaded runtime" +)] +pub async fn head_file(req: HttpRequest, store: web::Data) -> Result { + let (agent_id, filename) = agent_and_filename(&req)?; + let path = object_path(&agent_id, &filename); + + let meta = match store.head(&path).await { + Ok(meta) => meta, + Err(object_store::Error::NotFound { .. }) => return Err(StorageError::ObjectNotFound), + Err(error) => return Err(error.into()), + }; + + // `no_chunking` sets Content-Length to the object's size so a `HEAD` reports it exactly as the matching + // `GET` would, without sending a body. + let mut response = HttpResponse::Ok(); + let _typed = response.content_type("application/octet-stream"); + let _sized = response.no_chunking(meta.size); + insert_etag(&mut response, meta.e_tag.as_deref()); + Ok(response.finish()) } diff --git a/services/storage/tests/put.rs b/services/storage/tests/put.rs index f7f2c503..27e2648e 100644 --- a/services/storage/tests/put.rs +++ b/services/storage/tests/put.rs @@ -13,7 +13,7 @@ use std::collections::BTreeMap; use actix_web::dev::Payload as DevPayload; use actix_web::error::ResponseError as _; -use actix_web::http::StatusCode; +use actix_web::http::{Method, StatusCode, header}; use actix_web::{App, FromRequest as _, test, web}; use edge_toolkit::ws::AgentConnectionState; use edge_toolkit::ws_server::{AgentRecord, AgentRegistry}; @@ -138,6 +138,88 @@ async fn stores_an_image_and_returns_200_even_though_tty_rendering_cannot_succee assert_eq!(written, body); } +/// PUT returns an `ETag`, and GET and HEAD report the same entity tag; HEAD also reports the object size. +/// +/// Exercises the S3-compatible surface the storage service exposes: an S3 client stats an object with `HEAD` +/// (entity tag + `Content-Length`, no body) and reads its `ETag` off `GET`. The GET and HEAD tags are compared +/// to each other because both come from the stored object's metadata, so they match regardless of how a given +/// backend derives the tag on write. +#[actix_rt::test] +async fn get_and_head_expose_etag_and_head_reports_size() { + let tmp = tempfile::tempdir().unwrap(); + let config = storage_config(&tmp); + let registry = registry_with_agent("agent-1"); + let app = test::init_service( + App::new() + .app_data(web::Data::new(registry)) + .app_data(web::Data::new(config.clone())) + .configure(|cfg| configure::<()>(cfg, &config)), + ) + .await; + + let body = b"etag round-trip payload".as_ref(); + let put = test::TestRequest::put() + .uri("/storage/agent-1/payload.txt") + .set_payload(body) + .to_request(); + let put_resp = test::call_service(&app, put).await; + assert_eq!(put_resp.status(), StatusCode::OK); + assert!( + put_resp.headers().contains_key(header::ETAG), + "PUT should return an ETag" + ); + + let get = test::TestRequest::get() + .uri("/storage/agent-1/payload.txt") + .to_request(); + let get_resp = test::call_service(&app, get).await; + assert_eq!(get_resp.status(), StatusCode::OK); + let get_etag = get_resp.headers().get(header::ETAG).cloned(); + assert!(get_etag.is_some(), "GET should return an ETag"); + + let head = test::TestRequest::default() + .method(Method::HEAD) + .uri("/storage/agent-1/payload.txt") + .to_request(); + let head_resp = test::call_service(&app, head).await; + assert_eq!(head_resp.status(), StatusCode::OK); + assert_eq!( + head_resp.headers().get(header::ETAG).cloned(), + get_etag, + "HEAD ETag should match GET" + ); + assert_eq!( + head_resp + .headers() + .get(header::CONTENT_LENGTH) + .and_then(|value| value.to_str().ok()), + Some(body.len().to_string().as_str()), + "HEAD should report the object size as Content-Length" + ); +} + +/// HEAD on an object that was never stored is a 404, like GET. +#[actix_rt::test] +async fn head_missing_object_returns_404() { + let tmp = tempfile::tempdir().unwrap(); + let config = storage_config(&tmp); + let registry = registry_with_agent("agent-1"); + let app = test::init_service( + App::new() + .app_data(web::Data::new(registry)) + .app_data(web::Data::new(config.clone())) + .configure(|cfg| configure::<()>(cfg, &config)), + ) + .await; + + let head = test::TestRequest::default() + .method(Method::HEAD) + .uri("/storage/agent-1/absent.txt") + .to_request(); + let resp = test::call_service(&app, head).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); +} + #[actix_rt::test] async fn surfaces_io_failure_as_500() { // Block the *agent's* directory with a regular file, so the store's write fails on an ancestor that is not diff --git a/services/ws-modules/js-data1/README.md b/services/ws-modules/js-data1/README.md new file mode 100644 index 00000000..ac05eb62 --- /dev/null +++ b/services/ws-modules/js-data1/README.md @@ -0,0 +1,41 @@ +# js-data1 + +A JavaScript twin of the Rust [`data1`](../data1) module. It performs the same per-agent storage round-trip -- +connect as an agent, write a small text file to the agent's bucket, read it back, verify the bytes match -- but +drives storage through the [AWS SDK for JavaScript v3](https://github.com/aws/aws-sdk-js-v3) +(`@aws-sdk/client-s3`) instead of the generated REST client. + +## Why + +To prove that `et-storage-service` is already, in effect, an **anonymous S3-compatible interface**. An S3 client +is pointed at the existing service with: + +- `endpoint = ${httpBase}/storage` +- `forcePathStyle: true` +- bucket = `agent_id`, key = `filename` + +so `PutObject` / `GetObject` / `HeadObject` map straight onto the `PUT` / `GET` / `HEAD /storage/{agent_id}/{filename}` +routes. All three are verified end to end: the object round-trips byte-for-byte, and `PutObject` and `HeadObject` +both return an `ETag`. See `../../../s3.md` (repo parent dir) for the wider S3 migration notes. + +## Build + +`@aws-sdk/client-s3` pulls a large `@aws-sdk/*` + `@smithy/*` graph whose bare-specifier imports do not resolve +as served static files, so it is bundled into a single self-contained ESM with esbuild: + +```bash +MISE_ENV=js mise run build-ws-js-data1-module +``` + +This produces `pkg/et_ws_js_data1.js` (gitignored -- regenerated by the build task), which +`et-modules-service` serves at `/modules/et-ws-js-data1/`. + +## Run + +Headless, via the web runner (embedded Deno): + +```bash +MISE_ENV=js mise run cargo-test -- -p et-ws-web-runner --test modules js_data1 +``` + +or against a live server: `RUNNER_MODULE=et-ws-js-data1 mise run ws-web-runner`. diff --git a/services/ws-modules/js-data1/build.mjs b/services/ws-modules/js-data1/build.mjs new file mode 100644 index 00000000..2f4e8107 --- /dev/null +++ b/services/ws-modules/js-data1/build.mjs @@ -0,0 +1,17 @@ +// Bundle the js-data1 module (including its @aws-sdk/client-s3 dependency) into a single self-contained ESM +// that et-modules-service can serve as a static file. +// +// The esbuild JS API is used rather than the `esbuild` CLI bin: esbuild's postinstall overwrites its JS bin +// shim with the platform-native binary, which pnpm's node-based `.bin` wrapper then fails to execute +// ("Invalid or unexpected token"). The JS API loads the native binary itself and sidesteps that entirely. + +import { build } from "esbuild"; + +await build({ + bundle: true, + entryPoints: ["src/index.js"], + format: "esm", + outfile: "pkg/et_ws_js_data1.js", + platform: "browser", + target: "es2022", +}); diff --git a/services/ws-modules/js-data1/package.json b/services/ws-modules/js-data1/package.json new file mode 100644 index 00000000..bb32fa05 --- /dev/null +++ b/services/ws-modules/js-data1/package.json @@ -0,0 +1,17 @@ +{ + "name": "et-ws-js-data1", + "version": "0.1.0", + "description": "JS data1 twin using AWS SDK v3 (S3)", + "license": "Apache-2.0 OR MIT", + "type": "module", + "private": true, + "scripts": { + "build": "node build.mjs" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3" + }, + "devDependencies": { + "esbuild": "^0.25" + } +} diff --git a/services/ws-modules/js-data1/pkg/package.json b/services/ws-modules/js-data1/pkg/package.json new file mode 100644 index 00000000..47f1af0d --- /dev/null +++ b/services/ws-modules/js-data1/pkg/package.json @@ -0,0 +1,8 @@ +{ + "name": "et-ws-js-data1", + "type": "module", + "description": "data1 twin using AWS SDK v3 (S3)", + "version": "0.1.0", + "license": "Apache-2.0 OR MIT", + "main": "et_ws_js_data1.js" +} diff --git a/services/ws-modules/js-data1/src/index.js b/services/ws-modules/js-data1/src/index.js new file mode 100644 index 00000000..da0dea50 --- /dev/null +++ b/services/ws-modules/js-data1/src/index.js @@ -0,0 +1,145 @@ +// js-data1 -- a JavaScript twin of the Rust `data1` module that drives the per-agent storage round-trip +// through the AWS SDK for JavaScript v3 (`@aws-sdk/client-s3`) instead of the generated REST client. +// +// The point of this module is to prove that `et-storage-service` is an anonymous S3-compatible interface. We +// point a real S3 client at it -- endpoint `${httpBase}/storage`, path-style, bucket = agent_id, key = filename +// -- so `PutObject`/`GetObject`/`HeadObject` map straight onto the `PUT`/`GET`/`HEAD /storage/{agent_id}/{filename}` +// routes. All three are verified: the object round-trips byte-for-byte, and PUT/HEAD both return an `ETag`. +// +// The module contract (see services/ws-web-runner/src/runtime.rs and services/ws-server/static/app.js): export +// a `default` async init and an async `run`. `run` resolving == success (runner exits 0); `run` throwing == +// failure (runner exits non-zero). We throw only on the core round-trip failing, never on an S3 feature the +// service is simply missing. + +import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; + +const MODULE = "js-data1"; +const FILENAME = "test_data.txt"; + +// Log to the console (prefixed) and, in a real browser, mirror into the on-page status textarea. +function log(message) { + console.log(`[${MODULE}] ${message}`); + const el = globalThis.document?.getElementById?.("module-output"); + if (el) { + el.value += `${message}\n`; + } +} + +// Resolve the WebSocket URL: the runner-injected global first, else the page location, else localhost. +function websocketUrl() { + if (globalThis.__ET_WS_URL) { + return globalThis.__ET_WS_URL; + } + const loc = globalThis.location; + const proto = loc?.protocol === "https:" ? "wss:" : "ws:"; + const host = loc?.host ?? "localhost:8080"; + return `${proto}//${host}/ws`; +} + +// Resolve the HTTP base the storage service is served from (runner-injected global, else page origin). +function httpBase() { + if (globalThis.__ET_HTTP_BASE) { + return globalThis.__ET_HTTP_BASE; + } + return globalThis.location?.origin ?? "http://localhost:8080"; +} + +// Open a WebSocket, complete the `et-connect` handshake, and resolve with { ws, agentId }. +// +// Storage's `put_file` rejects any bucket that is not a *currently connected* agent, so we must hold this +// socket open across the PUT/GET. The server assigns the id in its `et-connect-ack` reply. +function connectAgent() { + const ws = new WebSocket(websocketUrl()); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("timed out waiting for et-connect-ack")), 10000); + ws.addEventListener("message", (event) => { + let frame; + try { + frame = JSON.parse(event.data); + } catch { + return; + } + if (frame.type === "et-connect-ack" && frame.agent_id) { + clearTimeout(timer); + resolve({ agentId: frame.agent_id, ws }); + } + }); + ws.addEventListener("error", () => { + clearTimeout(timer); + reject(new Error("websocket error before et-connect-ack")); + }); + ws.addEventListener("open", () => ws.send(JSON.stringify({ agent_id: null, type: "et-connect" }))); + }); +} + +// Build an S3 client aimed at et-storage-service. +// +// `endpoint` carries the `/storage` prefix and `forcePathStyle` makes the SDK address objects as +// `${endpoint}/${bucket}/${key}` -- i.e. `/storage/{agent_id}/{filename}`. The service is anonymous: it never +// validates the Authorization header, so we hand the SDK throwaway credentials (the resulting signature is +// ignored server-side) -- the most version-robust way to get v3 to emit the request. The checksum knobs are +// set to WHEN_REQUIRED to stop newer v3 defaults from wrapping the upload body in `aws-chunked` trailer +// framing, which the plain storage service would store verbatim and hand back on GET, breaking the round-trip. +function makeS3Client() { + return new S3Client({ + credentials: { accessKeyId: "anonymous", secretAccessKey: "anonymous" }, + endpoint: `${httpBase()}/storage`, + forcePathStyle: true, + region: "us-east-1", + requestChecksumCalculation: "WHEN_REQUIRED", + responseChecksumValidation: "WHEN_REQUIRED", + }); +} + +// Verify `HeadObject`: the storage service now serves HEAD with an ETag + Content-Length, so this is a +// checked step -- it throws (failing the run) if HEAD regresses or the ETag goes missing. +async function verifyHeadObject(s3, agentId) { + const head = await s3.send(new HeadObjectCommand({ Bucket: agentId, Key: FILENAME })); + log(`HeadObject ok: ContentLength=${head.ContentLength}, ETag=${head.ETag}`); + if (head.ETag === undefined) { + throw new Error("HeadObject returned no ETag"); + } +} + +// Init hook. Nothing to boot for a pure-JS module; the runner still requires the default export to exist. +export default function init() { + log("initialized"); +} + +// Execute the storage round-trip via the S3 client. Resolves on match; throws on mismatch or transport error. +export async function run() { + log("entered run()"); + const { agentId, ws } = await connectAgent(); + log(`connected as ${agentId}`); + + const s3 = makeS3Client(); + const content = `Hello from ${MODULE} at ${new Date().toISOString()}!`; + const body = new TextEncoder().encode(content); + + try { + log(`PutObject -> bucket=${agentId} key=${FILENAME} (${body.length} bytes)`); + const put = await s3.send( + new PutObjectCommand({ Body: body, Bucket: agentId, ContentType: "text/plain", Key: FILENAME }), + ); + if (put.ETag === undefined) { + throw new Error("PutObject returned no ETag"); + } + log(`PutObject ok: ETag=${put.ETag}`); + + log(`GetObject <- bucket=${agentId} key=${FILENAME}`); + const got = await s3.send(new GetObjectCommand({ Bucket: agentId, Key: FILENAME })); + const retrieved = await got.Body.transformToString(); + + if (retrieved !== content) { + log(`VERIFICATION FAILED: sent ${JSON.stringify(content)} but got ${JSON.stringify(retrieved)}`); + throw new Error("data mismatch"); + } + log("VERIFICATION SUCCESS - data matches!"); + + await verifyHeadObject(s3, agentId); + } finally { + s3.destroy(); + ws.close(); + } + log("workflow complete"); +} diff --git a/services/ws-web-runner/tests/modules.rs b/services/ws-web-runner/tests/modules.rs index d780ac0a..8b2fd133 100644 --- a/services/ws-web-runner/tests/modules.rs +++ b/services/ws-web-runner/tests/modules.rs @@ -72,6 +72,7 @@ use rstest::rstest; #[case::zig_except1("et-ws-zig-except1", Language::Zig)] #[case::dart_data1("et-ws-dart-data1", Language::Dart)] #[case::pywasm1("et-ws-pywasm1", Language::Python)] +#[case::js_data1("et-ws-js-data1", Language::Js)] fn module_runs_successfully(#[case] module: &str, #[case] language: Language) { // When CI narrows MISE_ENV (e.g. `dotnet,rust`) the env-gated guest // configs don't load and the matching `pkg/` never gets built. Skip @@ -88,6 +89,10 @@ fn module_runs_successfully(#[case] module: &str, #[case] language: Language) { println!("skipping {module}: pkg/ not built (build-ws-dotnet-data1-module has not run on this host)"); return; } + if module == "et-ws-js-data1" && !js_data1_pkg_built() { + println!("skipping {module}: pkg/ not built (build-ws-js-data1-module has not run on this host)"); + return; + } let server = et_ws_test_server::start(); run_runner_with_timeout(module, &server.ws_url, 90); #[cfg(feature = "coverage")] @@ -110,6 +115,21 @@ fn dotnet_data1_pkg_built() -> bool { .exists() } +/// Probe for js-data1's esbuild bundle, logging a skip instead of failing when absent. +/// +/// The AWS SDK v3 bundle `pkg/et_ws_js_data1.js` is generated by `build-ws-js-data1-module` and gitignored, so +/// on a checkout where that task has not run it is absent. Probe it and skip rather than 404 on the fetch. The +/// committed `pkg/package.json` is always present, so it is not a useful probe. +#[expect( + clippy::single_call_fn, + reason = "distinct probe step; kept named for the skip-trace log line" +)] +fn js_data1_pkg_built() -> bool { + edge_toolkit::config::get_project_root() + .join("services/ws-modules/js-data1/pkg/et_ws_js_data1.js") + .exists() +} + /// Spawn two runners against one ws-server and assert both finish ok. /// Used by communication modules that need to discover at least one peer /// via `et-list-agents` before they can complete (comm1, dart-comm1). diff --git a/utilities/int-gen/src/openapi.rs b/utilities/int-gen/src/openapi.rs index 4b67ddef..2005fa82 100644 --- a/utilities/int-gen/src/openapi.rs +++ b/utilities/int-gen/src/openapi.rs @@ -22,7 +22,10 @@ use crate::Error; info( title = "Edge Toolkit REST API", version = "0.1.0", - description = "ws-server HTTP surface: health probe, module discovery, module assets, per-agent storage." + description = "ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. +The storage routes are an anonymous S3-compatible interface -- addressed path-style as +/storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a +standard S3 client can read and write objects without credentials." ), servers( (url = "http://localhost:8080", description = "Default ws-server bind address") @@ -32,6 +35,7 @@ use crate::Error; et_modules_service::routes::list_modules_handler, et_modules_service::routes::get_module_file, et_storage_service::routes::get_file, + et_storage_service::routes::head_file, et_storage_service::routes::put_file::, ), components(schemas(et_ws_server::routes::HealthResponse)) From 94c80c66bbf78b1db77408c81b8ec9ca7168bc2b Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 6 Aug 2026 11:37:28 +0800 Subject: [PATCH 2/6] fix windows --- .mise/config.js.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.mise/config.js.toml b/.mise/config.js.toml index e205d900..0ff0e429 100644 --- a/.mise/config.js.toml +++ b/.mise/config.js.toml @@ -85,8 +85,11 @@ description = "Bundle the js-data1 module (AWS SDK for JS v3) into pkg/ with esb dir = "services/ws-modules/js-data1" # esbuild bundles @aws-sdk/client-s3 into one self-contained ESM. # Its @aws-sdk/* + @smithy/* graph uses bare-specifier imports that don't resolve as served static files, so -# bundling is required; pnpm install brings the workspace deps in first. -run = "pnpm install && pnpm run build" +# bundling is required; pnpm install brings the workspace deps in first. `node build.mjs` is invoked directly +# rather than via `pnpm run build`, because mise's standalone pnpm does not put mise's node on PATH for the +# script child on Windows (`'node' is not recognized`), whereas the mise task shell always has node (a `[tools]` +# entry) resolvable -- the same way `pnpm` itself resolves here. +run = "pnpm install && node build.mjs" [tasks.oxlint-check] description = "Lint JavaScript/TypeScript sources (oxlint)" From 0cabeb6d86d986c33a890d33901794e378cf17f9 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 6 Aug 2026 12:04:54 +0800 Subject: [PATCH 3/6] macos clang-tidy --- .mise/config.macos.toml | 7 +++++++ services/ws-modules/zig-except1/src/exceptions.cpp | 2 ++ 2 files changed, 9 insertions(+) diff --git a/.mise/config.macos.toml b/.mise/config.macos.toml index cfd07594..e83c4cfb 100644 --- a/.mise/config.macos.toml +++ b/.mise/config.macos.toml @@ -28,6 +28,13 @@ mac_libclang = "{{ exec(command='dirname $(dirname $(xcrun --find clang))') }}/l # already uses, so the .o files' stamped version and the linked binary's requested minimum agree and ld64.lld # has nothing to warn about -- fixing the mismatch instead of just silencing the lint that reports it. macosx_deployment_target = "{% if arch() == 'arm64' %}11.0{% else %}10.12{% endif %}" +# Point clang-tidy at conda:clang-tools' builtin-header resource dir so the zig/wasm32 pass finds stddef.h. +# On macOS clang-tidy cannot auto-locate its resource dir from the mise-installed conda binary the way Linux and +# Windows do -- both pass an explicit `-resource-dir` (config.linux.toml / config.windows.toml) while the +# config.toml default is empty -- so without this the clang-tidy-check pass fails with `'stddef.h' file not +# found`. The trailing `22` is clang's major version; bump it with the conda:clang-tools major (see the same +# note in config.windows.toml). +clang_resource_arg = "-resource-dir {{ env.HOME }}/.local/share/mise/installs/conda-clang-tools/latest/lib/clang/22" [env] # lld_flag points Apple's clang at ld64.lld (see [vars]). diff --git a/services/ws-modules/zig-except1/src/exceptions.cpp b/services/ws-modules/zig-except1/src/exceptions.cpp index 19dd6048..380bccea 100644 --- a/services/ws-modules/zig-except1/src/exceptions.cpp +++ b/services/ws-modules/zig-except1/src/exceptions.cpp @@ -95,6 +95,7 @@ const MinimalTypeInfo int_type_info = {nullptr, "i"}; namespace { // Throwing callee: the int payload is trivially destructible, so __cxa_throw records a null destructor. +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- num/den are named, order-obvious division operands. int32_t checked_divide(int32_t num, int32_t den) { if (den == 0) { throw den; @@ -107,6 +108,7 @@ int32_t checked_divide(int32_t num, int32_t den) { // Exception-safe boundary: returns the quotient, or -1 when checked_divide() throws. // The catch-all is the house rule above -- nothing may unwind past an extern "C" entry point into the Zig // caller. +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- num/den are named, order-obvious division operands. extern "C" int32_t try_divide(int32_t num, int32_t den) { try { return checked_divide(num, den); From de2ac76399e7a602170bcd0148c821f5e6aaea3f Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 6 Aug 2026 12:46:32 +0800 Subject: [PATCH 4/6] pin nightly --- .mise/config.coverage.toml | 12 ++++++------ .mise/config.mingw.toml | 2 +- .mise/config.msvc.toml | 2 +- .mise/config.toml | 21 ++++++++++++++++----- .mise/config.windows.toml | 2 +- generated/dart-rest/lib/rest_client.dart | 2 +- generated/rust-rest/src/lib.rs | 2 +- generated/specs/rest.yaml | 2 +- services/storage/Cargo.toml | 1 + utilities/int-gen/src/openapi.rs | 2 +- 10 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 07706664..05946ead 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -27,7 +27,7 @@ # It is set env-wide here because this env loads only under the coverage workflow; per-command RUSTFLAGS and # the minicov feature come from the wasm_cov / *_cov_feat vars so uninstrumented builds stay clean. [env] -RUSTUP_TOOLCHAIN = "nightly" +RUSTUP_TOOLCHAIN = "{{ vars.rust_nightly }}" # Resolve libonnxruntime at runtime from the mise install rather than from ort-sys's dylib copies. # ort-sys symlinks the ONNX Runtime dylibs next to the built binaries, but it derives that directory from # `OUT_DIR/../../..`, which nightly's `build///out` layout shifts down to `/debug/build`. @@ -135,7 +135,7 @@ if [ -z "$(find "$covdir" -maxdepth 1 -name '*.profraw' -print -quit 2>/dev/null exit 0 fi host="$(rustc -vV | goawk '/^host:/ { print $2 }')" -bin="$(rustc +nightly --print sysroot)/lib/rustlib/$host/bin" +bin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" gut="$covdir/gut.awk" coreutils cat > "$gut" <<'AWK' /^target datalayout/ { next } @@ -293,8 +293,8 @@ coreutils rm -f "$covdir/server-ready" wait "$server_pid" 2>/dev/null || true trap - EXIT -host="$(rustc +nightly -vV | goawk '/^host:/ { print $2 }')" -llbin="$(rustc +nightly --print sysroot)/lib/rustlib/$host/bin" +host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" +llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" gut="$covdir/gut.awk" coreutils cat > "$gut" <<'AWK' /^target datalayout/ { next } @@ -386,8 +386,8 @@ export CHROMEDRIVER="$driver" export LLVM_PROFILE_FILE="{{ config_root }}/$covdir/pic-viewer-%p.profraw" cargo test -p et-ws-pic-viewer --features et-web/coverage --target wasm32-unknown-unknown --test show_image -host="$(rustc +nightly -vV | goawk '/^host:/ { print $2 }')" -llbin="$(rustc +nightly --print sysroot)/lib/rustlib/$host/bin" +host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" +llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" gut="$covdir/gut.awk" coreutils cat > "$gut" <<'AWK' /^target datalayout/ { next } diff --git a/.mise/config.mingw.toml b/.mise/config.mingw.toml index 0cdb9f24..8dd01491 100644 --- a/.mise/config.mingw.toml +++ b/.mise/config.mingw.toml @@ -43,7 +43,7 @@ version = "stable-x86_64-pc-windows-gnullvm" components = "rust-src,rustfmt" profile = "minimal" targets = "wasm32-unknown-unknown,wasm32-wasip2,x86_64-pc-windows-gnu" -version = "nightly-x86_64-pc-windows-gnullvm" +version = "{{ vars.rust_nightly }}-x86_64-pc-windows-gnullvm" [vars] # Clear config.windows.toml's et-ws-web-runner exclusion. diff --git a/.mise/config.msvc.toml b/.mise/config.msvc.toml index 2158273e..7af93911 100644 --- a/.mise/config.msvc.toml +++ b/.mise/config.msvc.toml @@ -26,7 +26,7 @@ version = "stable-x86_64-pc-windows-gnullvm" components = "rust-src,rustfmt" profile = "minimal" targets = "wasm32-unknown-unknown,wasm32-wasip2,x86_64-pc-windows-msvc" -version = "nightly-x86_64-pc-windows-gnullvm" +version = "{{ vars.rust_nightly }}-x86_64-pc-windows-gnullvm" [vars] # portable-msvc script pin: gist id + revision (immutable content address) + sha256 of that revision. diff --git a/.mise/config.toml b/.mise/config.toml index c380da2e..58e36f9d 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -185,7 +185,7 @@ components = "rust-src,rustfmt,llvm-tools" os = ["linux", "macos"] profile = "minimal" targets = "wasm32-unknown-unknown,wasm32-wasip2" -version = "nightly" +version = "{{ vars.rust_nightly }}" # Native rustpython binary built from upstream RustPython main. # Built with `freeze-stdlib` (so the binary embeds the Python stdlib and doesn't need a RUSTPYTHONPATH dance) @@ -291,6 +291,16 @@ checksum = "sha256:6d97229c53cc0af09635bfd85b15169704ab9c07d5e23090252ac80746260 url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-x86_64-pc-windows-mingw.tar.gz" [vars] +# Rust nightly channel, pinned to a single version used for every nightly toolchain in the repo. +# One value drives the coverage build + its llvm-tools, cargo-fmt, cargo-doc-check, the et-rest-client fmt, and +# the Windows/mingw/msvc host toolchains, so no lane silently floats to a different nightly. Pinned because +# the 2026-08-05 nightly (rustc 7608eb7b0) regressed wasm coverage codegen -- minicov's `capture_coverage` reads +# the instrumented counter buffers out of bounds, so the guest traps with `wasm trap: out of bounds memory +# access` in `coverage::dump` and the et-ws-wasi-runner coverage tests (wasi-comm1/wasi-data1) fail. The +# 2026-08-04 nightly (rustc 1ed2df61a), which `nightly-2026-08-05` resolves to, is the last good one. minicov has +# shipped no release since 0.3.8 to absorb the codegen change, so pinning the toolchain is the only lever. Unpin +# (back to `nightly`) once a later nightly fixes the codegen or a new minicov handles it. +rust_nightly = "nightly-2026-08-05" # Tier-`a_` prefix marks leaf values referenced by later vars. # a_* vars are referenced by later vars (face1_src), and mise renders [vars] in alphabetical order; `a_` sorts # before every non-prefixed var name so the reference resolves. Shared GitHub release tag for the HF-asset @@ -677,10 +687,10 @@ run = "git ls-files '*Dockerfile' '*Dockerfile.*' | xargs hadolint --config conf run = "cargo check --workspace {{ vars.cargo_ws_excludes }}" [tasks.cargo-fmt] -run = "cargo +nightly fmt" +run = "cargo +{{ vars.rust_nightly }} fmt" [tasks.cargo-fmt-check] -run = "cargo +nightly fmt -- --check" +run = "cargo +{{ vars.rust_nightly }} fmt -- --check" [tasks.cargo-clippy-check] alias = ["cargo-clippy", "clippy", "clippy-check"] @@ -720,7 +730,8 @@ env = { RUSTDOCFLAGS = "-Z unstable-options --check -D warnings" } run = """ ort_build_dir="${CARGO_TARGET_DIR:-target}${CARGO_BUILD_TARGET:+/$CARGO_BUILD_TARGET}/debug/build" coreutils mkdir -p "$ort_build_dir/examples" "$ort_build_dir/deps" -cargo +nightly doc --keep-going --workspace --no-deps --document-private-items {{ vars.cargo_ws_excludes }} +tc="{{ vars.rust_nightly }}" +cargo "+$tc" doc --keep-going --workspace --no-deps --document-private-items {{ vars.cargo_ws_excludes }} """ shell = "bash -euo pipefail -c" @@ -1192,7 +1203,7 @@ description = "Run et-int-gen: emit the language-agnostic WS specs (YAML/WIT/KDL run = """ cargo run -q -p et-int-gen --bin et-int-gen -- generate core cargo run -q -p et-int-gen --bin et-int-gen -- generate rust -cargo +nightly fmt -p et-rest-client +cargo +{{ vars.rust_nightly }} fmt -p et-rest-client """ shell = "bash -euo pipefail -c" diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index 5d0a2c40..31322383 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -91,7 +91,7 @@ version = "stable-x86_64-pc-windows-gnullvm" components = "rust-src,rustfmt" profile = "minimal" targets = "wasm32-unknown-unknown,wasm32-wasip2" -version = "nightly-x86_64-pc-windows-gnullvm" +version = "{{ vars.rust_nightly }}-x86_64-pc-windows-gnullvm" [vars] # Forward-slashed mise installs root, for values spliced unquoted into bash task command lines. diff --git a/generated/dart-rest/lib/rest_client.dart b/generated/dart-rest/lib/rest_client.dart index 7723553d..00cdf3ab 100644 --- a/generated/dart-rest/lib/rest_client.dart +++ b/generated/dart-rest/lib/rest_client.dart @@ -11,7 +11,7 @@ import 'clients/storage.dart'; /// Edge Toolkit REST API `v0.1.0`. /// /// ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. -/// The storage routes are an anonymous S3-compatible interface -- addressed path-style as. +/// The storage routes are an anonymous S3-compatible interface, addressed path-style as. /// /storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a. /// standard S3 client can read and write objects without credentials. class RestClient { diff --git a/generated/rust-rest/src/lib.rs b/generated/rust-rest/src/lib.rs index d6f1170f..32095265 100644 --- a/generated/rust-rest/src/lib.rs +++ b/generated/rust-rest/src/lib.rs @@ -67,7 +67,7 @@ pub mod types { /**Client for Edge Toolkit REST API ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. -The storage routes are an anonymous S3-compatible interface -- addressed path-style as +The storage routes are an anonymous S3-compatible interface, addressed path-style as /storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a standard S3 client can read and write objects without credentials. diff --git a/generated/specs/rest.yaml b/generated/specs/rest.yaml index 023205c0..23034c96 100644 --- a/generated/specs/rest.yaml +++ b/generated/specs/rest.yaml @@ -3,7 +3,7 @@ info: title: Edge Toolkit REST API description: |- ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. - The storage routes are an anonymous S3-compatible interface -- addressed path-style as + The storage routes are an anonymous S3-compatible interface, addressed path-style as /storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a standard S3 client can read and write objects without credentials. version: 0.1.0 diff --git a/services/storage/Cargo.toml b/services/storage/Cargo.toml index 9c54da5f..a8df4dbd 100644 --- a/services/storage/Cargo.toml +++ b/services/storage/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "et-storage-service" +description = "E.T. anonymous S3-compatible service" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/utilities/int-gen/src/openapi.rs b/utilities/int-gen/src/openapi.rs index 2005fa82..fe05bba7 100644 --- a/utilities/int-gen/src/openapi.rs +++ b/utilities/int-gen/src/openapi.rs @@ -23,7 +23,7 @@ use crate::Error; title = "Edge Toolkit REST API", version = "0.1.0", description = "ws-server HTTP surface: health probe, module discovery, module assets, and per-agent storage. -The storage routes are an anonymous S3-compatible interface -- addressed path-style as +The storage routes are an anonymous S3-compatible interface, addressed path-style as /storage/{agent_id}/{filename} (bucket = agent_id, key = filename), they answer PUT/GET/HEAD with an ETag, so a standard S3 client can read and write objects without credentials." ), From 6b256e8b4d2d7b2d128f4910d14b1f299d302bfb Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 6 Aug 2026 13:48:50 +0800 Subject: [PATCH 5/6] try macos clang-tidy fix again --- .mise/config.macos.toml | 7 ------- .mise/config.zig.toml | 20 ++++++++++++++++++-- CLAUDE.md | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/.mise/config.macos.toml b/.mise/config.macos.toml index e83c4cfb..cfd07594 100644 --- a/.mise/config.macos.toml +++ b/.mise/config.macos.toml @@ -28,13 +28,6 @@ mac_libclang = "{{ exec(command='dirname $(dirname $(xcrun --find clang))') }}/l # already uses, so the .o files' stamped version and the linked binary's requested minimum agree and ld64.lld # has nothing to warn about -- fixing the mismatch instead of just silencing the lint that reports it. macosx_deployment_target = "{% if arch() == 'arm64' %}11.0{% else %}10.12{% endif %}" -# Point clang-tidy at conda:clang-tools' builtin-header resource dir so the zig/wasm32 pass finds stddef.h. -# On macOS clang-tidy cannot auto-locate its resource dir from the mise-installed conda binary the way Linux and -# Windows do -- both pass an explicit `-resource-dir` (config.linux.toml / config.windows.toml) while the -# config.toml default is empty -- so without this the clang-tidy-check pass fails with `'stddef.h' file not -# found`. The trailing `22` is clang's major version; bump it with the conda:clang-tools major (see the same -# note in config.windows.toml). -clang_resource_arg = "-resource-dir {{ env.HOME }}/.local/share/mise/installs/conda-clang-tools/latest/lib/clang/22" [env] # lld_flag points Apple's clang at ld64.lld (see [vars]). diff --git a/.mise/config.zig.toml b/.mise/config.zig.toml index 6a09a78e..066de521 100644 --- a/.mise/config.zig.toml +++ b/.mise/config.zig.toml @@ -83,7 +83,15 @@ llvm_ct="$(mise where 'github:mstorsjo/llvm-mingw')/bin/clang-tidy" git ls-files 'services/ws-web-runner/mingw-shim/*.c' | xargs -r -I{} "$llvm_ct" --config-file=config/clang-tidy.yaml {} -- --target=x86_64-w64-mingw32 -std=c11 # set -- (POSIX positional params), not a bash array: busybox ash on the Windows lane has no arrays. -set -- {{ vars.clang_resource_arg }} --target=wasm32-unknown-unknown -fwasm-exceptions -mexception-handling -fno-rtti +# macOS clang-tidy can't auto-locate its resource dir, and the conda:clang-tools package no longer ships the +# clang builtin headers (stddef.h etc.), so point -resource-dir at llvm-mingw's headers -- already installed for +# the mingw pass above, and the same set the Windows lane's clang_resource_arg uses. Linux keeps conda-clangxx's. +set -- --target=wasm32-unknown-unknown -fwasm-exceptions -mexception-handling -fno-rtti +if [ "$(uname -s)" = "Darwin" ]; then + set -- -resource-dir "$(mise where 'github:mstorsjo/llvm-mingw')/lib/clang/22" "$@" +else + set -- {{ vars.clang_resource_arg }} "$@" +fi git ls-files 'services/ws-modules/zig-*/src/*.c' 'services/ws-modules/zig-*/src/*.cpp' | xargs -r -I{} clang-tidy --config-file=config/clang-tidy.yaml {} -- "$@" """ @@ -101,7 +109,15 @@ llvm_ct="$(mise where 'github:mstorsjo/llvm-mingw')/bin/clang-tidy" git ls-files 'services/ws-web-runner/mingw-shim/*.c' | xargs -r -I{} "$llvm_ct" --fix --config-file=config/clang-tidy.yaml {} -- --target=x86_64-w64-mingw32 -std=c11 # set -- (POSIX positional params), not a bash array: busybox ash on the Windows lane has no arrays. -set -- {{ vars.clang_resource_arg }} --target=wasm32-unknown-unknown -fwasm-exceptions -mexception-handling -fno-rtti +# macOS clang-tidy can't auto-locate its resource dir, and the conda:clang-tools package no longer ships the +# clang builtin headers (stddef.h etc.), so point -resource-dir at llvm-mingw's headers -- already installed for +# the mingw pass above, and the same set the Windows lane's clang_resource_arg uses. Linux keeps conda-clangxx's. +set -- --target=wasm32-unknown-unknown -fwasm-exceptions -mexception-handling -fno-rtti +if [ "$(uname -s)" = "Darwin" ]; then + set -- -resource-dir "$(mise where 'github:mstorsjo/llvm-mingw')/lib/clang/22" "$@" +else + set -- {{ vars.clang_resource_arg }} "$@" +fi git ls-files 'services/ws-modules/zig-*/src/*.c' 'services/ws-modules/zig-*/src/*.cpp' | xargs -r -I{} clang-tidy --fix --config-file=config/clang-tidy.yaml {} -- "$@" """ diff --git a/CLAUDE.md b/CLAUDE.md index ccdafdbe..24f62fd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,6 +75,32 @@ everything's settled (waiting for the user to push): `/loop` dynamic-mode wakeups are bounded [60, 3600] by the runtime, so each cadence maps directly: 1 min -> `delaySeconds: 60`, 5 min -> `delaySeconds: 300`, 20 min -> `delaySeconds: 1200`. +## Reproduce a CI failure locally BEFORE fixing it + +When a CI check fails, reproduce the exact failure on your own machine **before** changing anything, then verify +the fix against that reproduction. Do NOT push a fix you have only reasoned about -- a blind fix that "should work" +wastes a full CI round-trip (10-30 min) per attempt and erodes trust when it lands still-red. + +"It passes when I run the task locally" is NOT a reproduction and NOT proof of a fix. CI runs on a **fresh** +environment; your machine carries months of accumulated state that silently masks the failure. The failures that +bite are environment differences your green local run hides: + +- **Floating toolchains/tools** (`nightly`, `latest`, unpinned `conda:`/`npm:`): CI installs today's version; yours is + whatever you installed weeks ago. A green local run just means your cached version predates the regression. To + reproduce, install the _exact_ version CI used (read it from the job log -- e.g. `rustc -Vv`, the `mise ... @X` + install lines) and run with that. +- **Stale install artifacts:** old `mise`/package installs leave symlinks (`.../latest`), directories, and even + bundled headers that a **fresh** install no longer creates or ships. A hardcoded path that resolves locally can be + a dead path on CI. Reproduce by removing the stale artifact (or `mise uninstall && mise install`) so your + layout matches a clean runner, then re-run. +- **OS-specific behaviour:** the failing lane may be macOS/Windows/Linux-specific. Reproduce on that OS (you are on + one of the tier-1 platforms); if you cannot, say so rather than guessing. + +The loop is: read the CI job log for the exact error + the exact versions -> recreate those conditions locally -> +confirm you see the identical failure -> fix -> confirm the fix turns that same local reproduction green -> only then +push. If you genuinely cannot reproduce locally (e.g. a lane you have no access to), say so explicitly and do not +pass off an unverified change as a fix. + ## Keep lines <= 120 characters `editorconfig-checker` (`ec`, wired into `mise run check` via the `editorconfig-check` task) enforces a 120-char From 135f3888628513492b0ef469ea1c1d3d8445f223 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 6 Aug 2026 15:57:52 +0800 Subject: [PATCH 6/6] rm mention of s3.md not in the repo --- CLAUDE.md | 11 +++++++++++ services/ws-modules/js-data1/README.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24f62fd4..2e228ca5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,17 @@ directory (which is already gitignored). Do **not** write to `/tmp`, `/var/tmp`, `~/scratch`, or any other path outside this working directory. `target/scratch/` is fine; create subdirectories under it freely and clean up when done. +### NEVER reference a file that isn't in this repo + +Do not mention, link to, or write a path to any file that is not tracked in this repo -- not from a tracked file +(source, `*.md`, config, a comment) and not in a commit message, PR body, or code-review note. A relative path +that escapes the repo root, an absolute filesystem path, or prose like "see the write-up in the parent directory" +is meaningless to everyone who clones the repo and reads as a dangling pointer. If the thing being referenced +matters to the repo, it must live **inside** the repo; if it deliberately lives elsewhere (the operator keeps it +outside the tree on purpose), then simply never point at it from inside. Creating or editing files outside the +repo is fine when the operator wants that -- this rule is **only** about never referring to out-of-repo files +from within the repo. + ## On macOS: use homebrew bash for ad-hoc agent commands On macOS, every ad-hoc command the agent runs that is **not** a `mise run ` invocation -- investigations, diff --git a/services/ws-modules/js-data1/README.md b/services/ws-modules/js-data1/README.md index ac05eb62..d22751a4 100644 --- a/services/ws-modules/js-data1/README.md +++ b/services/ws-modules/js-data1/README.md @@ -16,7 +16,7 @@ is pointed at the existing service with: so `PutObject` / `GetObject` / `HeadObject` map straight onto the `PUT` / `GET` / `HEAD /storage/{agent_id}/{filename}` routes. All three are verified end to end: the object round-trips byte-for-byte, and `PutObject` and `HeadObject` -both return an `ETag`. See `../../../s3.md` (repo parent dir) for the wider S3 migration notes. +both return an `ETag`. ## Build