apiVersion: jenkins.io/v1 kind: PipelineActivity metadata: annotations: lighthouse.jenkins-x.io/cloneURI: https://github.com/greencapitaltrade/bifrost.git lighthouse.jenkins-x.io/job: release pipeline.jenkins-x.io/traceID: 7ef2ee0c36ea291daf1b828a672403af creationTimestamp: "2026-05-16T20:19:53Z" generation: 7 labels: branch: main build: "87" context: release created-by-lighthouse: "true" event-GUID: 4beefb3a-5164-11f1-88cf-db06f7f9e1e2 lighthouse.jenkins-x.io/baseSHA: a63601d587fb6494c4e0324431d60af8ffa58e68 lighthouse.jenkins-x.io/branch: main lighthouse.jenkins-x.io/buildNum: "1778962791632" lighthouse.jenkins-x.io/context: release lighthouse.jenkins-x.io/id: apitaltrade-bifrost-main-release-6fxgm lighthouse.jenkins-x.io/job: release lighthouse.jenkins-x.io/lastCommitSHA: a63601d587fb6494c4e0324431d60af8ffa58e68 lighthouse.jenkins-x.io/refs.org: greencapitaltrade lighthouse.jenkins-x.io/refs.repo: bifrost lighthouse.jenkins-x.io/type: postsubmit owner: greencapitaltrade podName: apitaltrade-bifrost-main-release-zgr4k-from-build-pack-pod provider: github repository: bifrost tekton.dev/pipeline: apitaltrade-bifrost-main-release-zgr4k managedFields: - apiVersion: jenkins.io/v1 fieldsType: FieldsV1 fieldsV1: f:metadata: f:labels: f:branch: {} f:context: {} f:owner: {} f:provider: {} f:repository: {} f:spec: f:lastCommitMessage: {} f:releaseNotesURL: {} f:version: {} manager: jx-changelog-0.10.18 operation: Update time: "2026-05-16T20:24:17Z" - apiVersion: jenkins.io/v1 fieldsType: FieldsV1 fieldsV1: f:metadata: f:annotations: .: {} f:lighthouse.jenkins-x.io/cloneURI: {} f:lighthouse.jenkins-x.io/job: {} f:pipeline.jenkins-x.io/traceID: {} f:labels: .: {} f:build: {} f:created-by-lighthouse: {} f:event-GUID: {} f:lighthouse.jenkins-x.io/baseSHA: {} f:lighthouse.jenkins-x.io/branch: {} f:lighthouse.jenkins-x.io/buildNum: {} f:lighthouse.jenkins-x.io/context: {} f:lighthouse.jenkins-x.io/id: {} f:lighthouse.jenkins-x.io/job: {} f:lighthouse.jenkins-x.io/lastCommitSHA: {} f:lighthouse.jenkins-x.io/refs.org: {} f:lighthouse.jenkins-x.io/refs.repo: {} f:lighthouse.jenkins-x.io/type: {} f:podName: {} f:tekton.dev/pipeline: {} f:spec: .: {} f:baseSHA: {} f:batchPipelineActivity: {} f:build: {} f:buildLogsUrl: {} f:completedTimestamp: {} f:context: {} f:gitBranch: {} f:gitOwner: {} f:gitRepository: {} f:gitUrl: {} f:lastCommitSHA: {} f:message: {} f:pipeline: {} f:startedTimestamp: {} f:status: {} f:steps: {} manager: jx-build-controller operation: Update time: "2026-05-16T20:25:24Z" name: greencapitaltrade-bifrost-main-87 namespace: jx resourceVersion: "24888310" uid: 70a12de9-7b4e-4413-b2e5-1f027dc7e0c5 spec: baseSHA: a63601d587fb6494c4e0324431d60af8ffa58e68 batchPipelineActivity: {} build: "87" buildLogsUrl: s3://logs-gct-prod-20260425045301534100000007/jenkins-x/logs/greencapitaltrade/bifrost/main/87.log completedTimestamp: "2026-05-16T20:25:17Z" context: release gitBranch: main gitOwner: greencapitaltrade gitRepository: bifrost gitUrl: https://github.com/greencapitaltrade/bifrost.git lastCommitMessage: |- feat(asset_management): VoltCred BLE capability token mint API (#135) * feat(asset_management): VoltCred BLE capability token mint API Adds the server-side counterpart of Wasp's BLE_COMMAND_AUTH.md: mobile apps call Fury -> Fury proxies to Bifrost -> Bifrost mints a signed 148-byte token bound to (user, asset, app pubkey). Wire format and trust model documented in greencapitaltrade/wasp:BLE_COMMAND_AUTH.md. Files: * models/asset_ble_token.py - 148-byte signed wire format (struct-packed, little-endian) - Ed25519 signing using ir.config_parameter 'voltcred.ble_root_priv_key_hex' (populated from Azure Key Vault by the boot job in production) - Monotonic token_id via ir.sequence + 32-bit time prefix - Server-side perm filtering (reserved bits dropped) and pin_required_perms intersection with granted perms - validity cap at 24h, enforced server-side - revoke() + cron_mark_expired() lifecycle * controllers/asset_ble_token_api.py - POST /api/asset/ble-token/mint - POST /api/asset/ble-token//revoke - POST /api/asset/ble-token/list/ - Matches the asset_command_api auth pattern: api_key auth, Fury supplies uid, Bifrost creates user-scoped env so record rules apply * security/asset_ble_token_security.xml - group_ble_token_mint group + record rules - Admin bypass for support / audit * security/ir.model.access.csv - Read for any internal user, manage for the mint group, full for system * data/ble_token_cron.xml - Hourly expiry sweep * tests/test_ble_token.py - Wire format byte-level assertions (offsets, total length 148, signed prefix 84, sig 64) - Round-trip signature verification with the root pub key - Tampering invalidates signature - Monotonic token_id - Reserved-bit perm filtering - PIN subset intersection - PIN-required without verifier rejected - Validity cap enforcement - Revoke is idempotent - cron expiry transitions only past-due tokens - Missing root key produces clear error - Asset without IMEI rejected at mint Tests live in tests/test_ble_token.py — run with docker compose exec fleet-bifrost pytest \ addons/asset_management/tests/test_ble_token.py or docker compose exec fleet-bifrost /usr/bin/odoo \ -u asset_management -d bifrost \ --test-enable --test-tags asset_ble_token --stop-after-init Co-Authored-By: Claude Opus 4.7 * fix(asset_management): root key sourced from env var (ASM via JX boot) Replace the ir.config_parameter-only sourcing with an env-var-first strategy that matches the standard secret flow: ASM secret value -> JX boot reads .jx/secret/mapping/secret-mappings.yaml -> Plain v1/Secret created on the cluster -> Bifrost pod mounts it as VOLTCRED_BLE_ROOT_PRIV_KEY_HEX -> _load_root_private_key reads os.environ Falls back to ir.config_parameter for dev / test only. The env var always wins when set, so production cannot accidentally use a stale DB-resident key. Memory: feedback_no_azure_use_asm.md (no more Azure references) and feedback_mcu_secrets_pattern.md (no ExternalSecret CRDs). Test update: test_missing_root_key_yields_clear_error now unsets the env var as well as clearing the config parameter. Co-Authored-By: Claude Opus 4.7 * chore(charts): expose VOLTCRED_BLE_ROOT_PRIV_KEY_HEX through Bifrost Secret The asset_management addon's BLE token mint endpoint reads this env var to sign tokens (see models/asset_ble_token.py _load_root_private_key). Wire it through the chart so JX boot can populate it from AWS Secrets Manager into the production-bifrost-bifrost v1/Secret, which the deployment already consumes via envFrom secretRef. Operator steps to activate: 1. Generate the keypair on a trusted workstation: python3 -c "from nacl.signing import SigningKey; \ sk = SigningKey.generate(); \ print('PUB :', sk.verify_key.encode().hex()); \ print('PRIV:', sk.encode().hex())" 2. Put the PRIV hex into AWS Secrets Manager: aws secretsmanager update-secret \ --secret-id production-bifrost-bifrost \ --secret-string "$(aws secretsmanager get-secret-value \ --secret-id production-bifrost-bifrost \ --query SecretString --output text \ | jq --arg k "$PRIV" \ '. + {VOLTCRED_BLE_ROOT_PRIV_KEY_HEX: $k}')" 3. Put the PUB hex into the Wasp firmware's a7677s-firmware/auth/server_root_key.c and ship a signed OTA. 4. Roll Bifrost pods so envFrom picks up the new value. 5. Keep the PRIV hex offline forever — backup once, then delete the local copy. Rotation = regenerate, re-OTA the device fleet. Co-Authored-By: Claude Opus 4.7 * fix(asset_management): make BLE token tests pass on Odoo 18 devcontainer Four fixes to get `--test-tags asset_ble_token` green (0 failed, 0 errors of 20): 1. data/ble_token_cron.xml: Odoo 18 dropped `numbercall` / `doall` on ir.cron. Switched to `priority` to match the surrounding cron files in this addon. 2. models/asset_ble_token.py: asset.asset does not carry an `imei` field — IMEI lives on `asset.iot.device.device_id` via FK to the asset. Added `_resolve_asset_imei(asset)` that fetches the first linked device's `device_id`. Mint rejects with a clear UserError when no IoT device is attached. 3. models/asset.py: pre-existing typo in `_compute_finance_status` — `asset_rec.total_outstanding` -> `rec.total_outstanding`. Caught by the test runner's flush_all cycle. Not BLE-related but per CLAUDE.md we don't dismiss pre-existing failures. 4. tests/test_ble_token.py: corrected model names (`asset.model.brand` / `asset.model`, not `asset.vehicle.model.*`); added IoT-device creation in setUpClass so the mint flow has an IMEI to bind against. Test run inside docker-compose -f docker-compose.test.yml: odoo -d bifrost --test-enable --test-tags asset_ble_token \ --stop-after-init -> 0 failed, 0 error(s) of 20 tests Co-Authored-By: Claude Opus 4.7 * feat(asset_management): VoltCred BLE Option B server-token pipeline Adds the Bifrost-side machinery for issue #136 — unifying cellular command auth with the BLE capability-token model so the server itself acts as a special "app". Model changes - asset.ble.token.mint_server_token: 1-year cap, full perms, no PIN, app_id=0 marker, audit-trailed to a designated system user. - asset.ble.token.push_server_token_via_mqtt: publishes the signed 148-byte blob to vc//server-token, retained, QoS 1. - asset.ble.token.cron_publish_server_tokens: 7-day sweep, refreshes tokens within 30 days of expiry, retires the previous token. - asset.kafka.publisher.publish_mqtt: structured Kafka envelope on mqtt.publish topic for the bridge to forward. - asset.iot.device.create hook: auto-mints + publishes a server token when a device with an IMEI is provisioned. Skipped via context flag for tests, and silenced when feature flag is off. - asset.iot.device.server_token_installed (new Boolean, sticky): set by telemetry status payload. Drains queued commands when it flips. - asset.iot.command.log waiting_for_token state + dispatch helper: /api/asset/commands//execute now queues commands when the device hasn't confirmed install, instead of dispatching dead. Refactor - Extracted mint() workhorse so mint() and mint_server_token() share packing/signing/record-creation while picking different validity caps and audit-trail users. - Drive-by fix: primary_metric_field typo in _get_primary_metric (was primary_device_metric_field — model doesn't have that field). Feature flag - voltcred.ble_option_b_enabled (ir.config_parameter, default False). All new behaviour is dark until ops flips it on. Gate is checked by the create hook, the cron, and the command dispatch path. Tests - 24 new tests in test_ble_token_server.py covering wire format, perm bitmap, app_id=0 marker, audit trail, signature verification, validity caps, MQTT publish (topic / retained / QoS / payload bytes / no-IMEI rejection), create-hook behaviour with flag on/off, cron refresh + skip windows, and the install-bit handshake (defaults, stickiness, queue + drain). - All 24 pass; full asset_management suite shows zero new regressions (parent branch already has 7 fail / 145 errors from the in-flight vehicle→asset rename, unrelated). Companion: deadpool#31 (frame builder + Redis nonce counter). Co-Authored-By: Claude Opus 4.7 * chore(charts): expose VC_SERVER_CMD_PRIV_KEY_HEX through Bifrost Secret Adds the Option B server-commands private key (issue #136) to the Bifrost chart's Secret template, alongside the existing BLE root key. Bifrost itself doesn't read VC_SERVER_CMD_PRIV_KEY_HEX — it surfaces the value so the Deadpool pod can mount it for cellular command signing. The default is "" so the secret stays harmless until JX boot pulls the real value from ASM via secret-mappings.yaml. Co-Authored-By: Claude Opus 4.7 * feat(crypto): use ECDSA P-256 (not Ed25519) for BLE token signing In-place update of PR #135 to ship as ECDSA from day one rather than shipping Ed25519 and migrating later. The production secure element (ATECC608B-SSHDA-T) only supports the NIST P-256 curve; this PR has not yet merged so there's no fleet to migrate — single PR, single review, no transition window. What changed: Crypto primitive - cryptography.hazmat.ed25519 → cryptography.hazmat.asymmetric.ec.SECP256R1 + hashes.SHA256 - sign() emits DER → re-encoded to raw r||s big-endian (64 B) so the on-device verifier (micro-ecc) and Deadpool's p256 crate see the same bytes. DER never crosses the wire. Wire format (matches wasp PR #5 + deadpool PR #37) - app_pubkey: 32 B Ed25519 → 64 B raw X || Y (no SEC1 0x04 prefix) - signature: stays 64 B (raw r || s, was Ed25519's 64 B) - token total: 148 → 180 bytes - signed prefix: 84 → 116 bytes - all offsets after app_pubkey shift +32 Key sourcing Env-var VOLTCRED_BLE_ROOT_PRIV_KEY_HEX kept the same. The bytes are now interpreted as a 32-B big-endian P-256 private-value scalar (was an Ed25519 seed). No secret rename needed in ASM. Files M models/asset_ble_token.py — _p256() shim, _raw_rs_from_der, _pack_signed_prefix layout, _sign + _load_root_private_key, _generate_root_keypair_for_test M controllers/asset_ble_token_api.py — docstring only M tests/test_ble_token.py — new offsets + ECDSA verify path (raw r||s → DER round-trip) M tests/test_ble_token_server.py — server-token pubkey offset 12..76, signature verify ECDSA-flavoured Tests (28/28 pass in bifrost-test): - test_wire_format_total_length_is_180 - test_wire_format_field_offsets (proves every offset matches firmware token_store.c OFF_*) - test_signature_verifies_with_root_pubkey ↳ Bifrost mints, test verifies. Round-trip proves the raw r||s bytes Bifrost ships are the same shape the firmware will read. - test_tampering_signed_prefix_invalidates_signature - test_mint_server_token_signature_verifies (ECDSA flavour) - test_mint_server_token_embeds_server_pubkey at offset 12..76 - All permission, validity, lifecycle, and IMEI tests unchanged and still passing under the new wire shape. Companion PRs (all open): - wasp PR #5 — firmware verifier (micro-ecc) - deadpool PR #37 — signer (p256 crate) + RFC 6979 cross-end vector Co-Authored-By: Claude Opus 4.7 * test(ble_token): add ECDSA edge cases (off-curve, k-randomness, length bounds) Five new tests covering failure modes the existing offset/round-trip suite didn't exercise. Each one anchored to a real risk: test_pubkey_63_bytes_rejected 63-byte pubkey (one short of valid raw X || Y) must be rejected. Without the _decode_pubkey length check, _pack_signed_prefix would build a misaligned token and every later byte would be off-by-one. test_pubkey_65_bytes_rejected 65-byte pubkey is the SEC1 uncompressed form (0x04 || X || Y) — a natural mistake for a mobile app. Our wire expects raw 64 B. Test pins the rejection so it's intentional, not a bug. test_two_mints_with_same_inputs_produce_different_signatures cryptography.hazmat ECDSA uses randomised k by default (NOT RFC 6979 deterministic like the p256 Rust crate). Two mints must produce different signatures. If they ever match, signing is broken. test_mint_signature_uses_raw_rs_not_der On-device verifier (micro-ecc) requires raw r||s. If we ever accidentally ship DER, the bytes would start with 0x30 (SEQUENCE tag) and the firmware would silently reject every command. Asserts both length=64 AND that the bytes don't parse as DER. test_mint_then_verify_with_recorded_app_pubkey_roundtrip End-to-end: mint → re-read app_pubkey_hex from the stored record → confirm wire bytes match → verify signature against root pubkey. Proves the record-read path is consistent with what gets shipped to the device. Why these and not others: TDD-first would have surfaced exactly these risks (wire-format mistakes, signature-encoding mistakes, randomness contract). The earlier offset-checking tests pass for the right reason; these new tests fail for clear reasons if the underlying invariant breaks. 33/33 pass in bifrost-test. Co-Authored-By: Claude Opus 4.7 * retrigger: previous CI pod-selection race (exit 137 from rolling deploy mid-exec) --------- Co-authored-by: Claude Opus 4.7 Co-authored-by: gct-bot <50795383+gct-bot@users.noreply.github.com> lastCommitSHA: a63601d587fb6494c4e0324431d60af8ffa58e68 message: 'Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 0' pipeline: greencapitaltrade/bifrost/main releaseNotesURL: https://github.com/greencapitaltrade/bifrost/releases/tag/v6.62.0 startedTimestamp: "2026-05-16T20:19:53Z" status: Succeeded steps: - kind: Stage stage: completedTimestamp: "2026-05-16T20:25:17Z" name: from build pack startedTimestamp: "2026-05-16T20:20:05Z" status: Succeeded steps: - completedTimestamp: "2026-05-16T20:20:23Z" name: Git Clone startedTimestamp: "2026-05-16T20:20:05Z" status: Succeeded - completedTimestamp: "2026-05-16T20:20:27Z" name: Next Version startedTimestamp: "2026-05-16T20:20:23Z" status: Succeeded - completedTimestamp: "2026-05-16T20:20:30Z" name: Jx Variables startedTimestamp: "2026-05-16T20:20:27Z" status: Succeeded - completedTimestamp: "2026-05-16T20:20:31Z" name: Setup Npm Nexus startedTimestamp: "2026-05-16T20:20:31Z" status: Succeeded - completedTimestamp: "2026-05-16T20:20:34Z" name: Setup Pip Cache startedTimestamp: "2026-05-16T20:20:31Z" status: Succeeded - completedTimestamp: "2026-05-16T20:20:35Z" name: Process Config Templates startedTimestamp: "2026-05-16T20:20:34Z" status: Succeeded - completedTimestamp: "2026-05-16T20:20:35Z" name: Update Fleet Management Version startedTimestamp: "2026-05-16T20:20:35Z" status: Succeeded - completedTimestamp: "2026-05-16T20:20:38Z" name: Check Registry startedTimestamp: "2026-05-16T20:20:35Z" status: Succeeded - completedTimestamp: "2026-05-16T20:23:57Z" name: Build Container Build startedTimestamp: "2026-05-16T20:20:39Z" status: Succeeded - completedTimestamp: "2026-05-16T20:24:17Z" name: Promote Changelog startedTimestamp: "2026-05-16T20:23:58Z" status: Succeeded - completedTimestamp: "2026-05-16T20:24:20Z" name: Promote Helm Release startedTimestamp: "2026-05-16T20:24:17Z" status: Succeeded - completedTimestamp: "2026-05-16T20:25:17Z" name: Promote Jx Promote startedTimestamp: "2026-05-16T20:24:21Z" status: Succeeded - kind: Promote promote: environment: staging pullRequest: pullRequestURL: https://github.com/greencapitaltrade/mcu/pull/3887 startedTimestamp: "2026-05-16T20:24:52Z" status: Succeeded startedTimestamp: "2026-05-16T20:24:52Z" status: Succeeded - kind: Promote promote: environment: production pullRequest: pullRequestURL: https://github.com/greencapitaltrade/mcu/pull/3888 startedTimestamp: "2026-05-16T20:25:14Z" status: Succeeded startedTimestamp: "2026-05-16T20:25:14Z" status: Succeeded version: 6.62.0 status: {}