Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f283e6a
fix: serialize tenant connector admission and expose capacity
Proxicon Oct 2, 2026
e320d56
feat(flows): preserve historical connector feature over current monit…
Oct 6, 2026
aab8a81
feat(flows): compose current shared M2M receiver and physical proof s…
Oct 6, 2026
c81f6d7
fix(flows): preserve mandatory admission and clock floor for receiver…
Oct 6, 2026
4d3e819
merge: carry genuine current150 model and continuity into receiver so…
Oct 6, 2026
99a2292
merge: carry genuine post-save Flow regression into receiver source
Oct 6, 2026
05144ee
chore(migrations): prepare isolated historical Connector baseline model
Oct 6, 2026
e8cc250
chore(migrations): retain genuine Connector union and restore receive…
Oct 6, 2026
f4039c0
feat(flows): add genuine incremental receiver evidence migration and …
Oct 6, 2026
e358efb
test(flows): bind receiver provider fixture to current schema and liv…
Oct 6, 2026
72e13e1
fix(flows): preserve uncertainty for invalid receiver responses
Oct 6, 2026
281a974
test(flows): align recovery clocks and connector UI events
Oct 6, 2026
6c536ef
test(migrations): preserve receiver upgrade and scoped rollback conti…
Oct 6, 2026
bf73dc3
test(flows): respect post-pairing admission before physical task
Oct 6, 2026
b1bef8c
merge: inherit verified companion pin for connector CI
Oct 6, 2026
112896b
test(compose): observe the actual setup restart operation
Oct 6, 2026
6a50e3a
test(gateway): await bounded timer disposal before sampling resources
Oct 6, 2026
41c2a10
test(flows): stream actual telemetry through the physical fixture proxy
Oct 6, 2026
2c823c6
test(monitoring): select the seeded tenant and exact permission expec…
Oct 6, 2026
b973663
ci: overlap backend tests and drain them before browser execution
Oct 6, 2026
13d08c1
test: preserve capability JSON and observe owned API startup failures
Oct 6, 2026
94f62ca
fix(monitoring): return rule editor focus to its launcher
Oct 6, 2026
81cc7a9
Merge actual Main into early private PR152 diagnostic preparation
Oct 6, 2026
81a8b09
test: record first-run login state after existing browser failure
Oct 6, 2026
e58e123
test: isolate owned physical API host inputs and report REST binding
Oct 6, 2026
4e2582a
Merge current Main after durable Monitoring for early connector valid…
Oct 7, 2026
3d03923
ci: defer Core tests until physical proof finishes
Oct 7, 2026
3f3cbf2
ci: preserve scheduling contracts in existing static checks
Oct 7, 2026
19d44cb
Merge main into early draft incident delivery
Oct 7, 2026
58ee0c6
ci: require ripgrep for PostgreSQL repository scans
Oct 7, 2026
aa38892
Merge actual Main151 into incident delivery
Oct 7, 2026
4c5062b
fix: accept opaque approved service link references
Oct 7, 2026
a7d63a7
test: pin verified published RatelDesk companion beta.16
Oct 7, 2026
82efa07
test(service-links): fix peer readiness and restart recovery fixtures
Oct 7, 2026
69d3042
fix(ci): retain browser budget and bind published receiver identity
Oct 7, 2026
1294864
test(web): capture monitoring startup failures before cleanup
Oct 7, 2026
0cd3ae5
ci: overlap independent checks and trace physical test stages
Oct 7, 2026
979e832
Merge main into RatelDesk connector: preserve fast CI policy and publ…
Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ jobs:
permissions:
contents: read
packages: write
actions: read
strategy:
fail-fast: false
matrix:
Expand All @@ -183,6 +184,7 @@ jobs:
env:
VERSION: ${{ needs.verify.outputs.version }}
REVISION: ${{ needs.verify.outputs.revision }}
RELEASE_BUILD_RUN_ID: ${{ needs.verify.outputs.run_id }}
NETRATEL_RELEASE_SOURCE_ROOT: ${{ github.workspace }}/source
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -193,7 +195,7 @@ jobs:
with:
ref: ${{ needs.verify.outputs.tag }}
path: source
- name: Build, scan, and push approved image
- name: Build, scan, and journal approved image before push
shell: bash
run: |
set -euo pipefail
Expand All @@ -210,11 +212,65 @@ jobs:
--version "$VERSION" --revision "$REVISION"
bash tools/ci/scan-public-image.sh \
--image "$tag" --version "$VERSION" --revision "$REVISION"
mkdir -p "$RUNNER_TEMP/image-journal" "$RUNNER_TEMP/review-inputs"
gh run download "$RELEASE_BUILD_RUN_ID" --repo "$GITHUB_REPOSITORY" \
--name "netratel-${VERSION}-linux-x64" \
--name "netratel-client-${VERSION}-linux-x64" \
--name "netratel-client-${VERSION}-win-x64" \
--name "netratel-client-${VERSION}-osx-arm64" \
--dir "$RUNNER_TEMP/review-inputs"
python3 "$GITHUB_WORKSPACE/tooling/tools/ci/promote-release.py" receipt \
--inputs "$RUNNER_TEMP/review-inputs" \
--output "$RUNNER_TEMP/image-journal/original-input-receipt.json" \
--run-id "$RELEASE_BUILD_RUN_ID"
local_config="$(docker image inspect "$tag" --format '{{.Id}}')"
python3 "$GITHUB_WORKSPACE/tooling/tools/ci/promote-release.py" image-journal \
--inputs "$RUNNER_TEMP/review-inputs" \
--receipt "$RUNNER_TEMP/image-journal/original-input-receipt.json" \
--output "$RUNNER_TEMP/image-journal/prepared.json" \
--component "$component" --phase prepared --local-image-config "$local_config"
- name: Preserve authenticated prepared image journal before registry write
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: publication-image-${{ matrix.component }}-prepared-attempt-${{ github.run_attempt }}
path: |
${{ runner.temp }}/image-journal/prepared.json
${{ runner.temp }}/image-journal/original-input-receipt.json
if-no-files-found: error
retention-days: 14
- name: Push approved image and journal exact published digest
shell: bash
run: |
set -euo pipefail
cd "$NETRATEL_RELEASE_SOURCE_ROOT"
test "$(git rev-parse HEAD)" = "$REVISION"
component='${{ matrix.component }}'
tag="ghcr.io/bostontechnologies/netratel-${component}:${VERSION}"
# Recheck immediately before the write; a prepared journal does not
# authorize replacing or repairing an existing remote image tag.
python3 "$GITHUB_WORKSPACE/tooling/tools/ci/promote-release.py" registry-check \
--version "$VERSION" --component "$component"
docker push "$tag"
digest="$(docker buildx imagetools inspect "$tag" --format '{{json .Manifest.Digest}}' | tr -d '"')"
[[ "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]
printf '%s\n' "ghcr.io/bostontechnologies/netratel-${component}@${digest}" \
> "$RUNNER_TEMP/${component}.digest"
cp "$RUNNER_TEMP/${component}.digest" "$RUNNER_TEMP/image-journal/${component}.digest"
local_config="$(docker image inspect "$tag" --format '{{.Id}}')"
python3 "$GITHUB_WORKSPACE/tooling/tools/ci/promote-release.py" image-journal \
--inputs "$RUNNER_TEMP/review-inputs" \
--receipt "$RUNNER_TEMP/image-journal/original-input-receipt.json" \
--output "$RUNNER_TEMP/image-journal/digest-recorded.json" \
--component "$component" --phase digest-recorded \
--local-image-config "$local_config" --image-digest "$(cat "$RUNNER_TEMP/${component}.digest")"
- name: Preserve image phase evidence after success or failure
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: publication-image-${{ matrix.component }}-result-attempt-${{ github.run_attempt }}
path: ${{ runner.temp }}/image-journal
if-no-files-found: error
retention-days: 14
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: promoted-${{ matrix.component }}-digest
Expand Down Expand Up @@ -304,6 +360,16 @@ jobs:
--output "$RUNNER_TEMP/promoted-release" \
--state "$RUNNER_TEMP/promotion-state.json"
(cd "$RUNNER_TEMP/promoted-release" && sha256sum -c SHA256SUMS)
- name: Preserve exact completed bytes before public asset publication
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: publication-finish-prepared-attempt-${{ github.run_attempt }}
path: |
${{ runner.temp }}/release-receipt.json
${{ runner.temp }}/promotion-state.json
${{ runner.temp }}/promoted-release
if-no-files-found: error
retention-days: 14
- name: Attach and verify public release assets
shell: bash
run: |
Expand All @@ -316,3 +382,14 @@ jobs:
gh release download "v${VERSION}" --repo "$GITHUB_REPOSITORY" \
--dir "$RUNNER_TEMP/public-downloads"
(cd "$RUNNER_TEMP/public-downloads" && sha256sum -c SHA256SUMS)
- name: Preserve exact Finish receipt state and staged bytes after success or failure
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: publication-finish-attempt-${{ github.run_attempt }}
path: |
${{ runner.temp }}/release-receipt.json
${{ runner.temp }}/promotion-state.json
${{ runner.temp }}/promoted-release
if-no-files-found: error
retention-days: 14
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ normal operation.
- **CLI**, **stdio MCP**, and **HTTP MCP** provide separately authorized
automation interfaces.
- **Client** is the native agent installed on managed machines.
- **RatelDesk connector** provides tenant-scoped protected configuration and
preview for monitoring flows; see [Monitoring flows](docs/FLOWS.md).

## Self-hosting

Expand Down Expand Up @@ -60,12 +62,18 @@ test with:
```sh
dotnet restore NetRatel.sln
dotnet build NetRatel.sln --configuration Release --no-restore
dotnet test NetRatel.sln --configuration Release --no-build
bash tools/ci/run-fast-regressions.sh TestResults/fast
```

`tools/ci/verify-product-version.sh` verifies that all first-party projects
evaluate to the release-manifest version.

Required CI runs fast unit, component and focused API/PostgreSQL regressions
after one restore/build. Full service-link pairs, physical incidents, native
client execution, browser acceptance and deployment/upgrade checks are opt-in
through the manual integration workflow. See [release validation](docs/RELEASES.md)
for selecting one functional suite and interpreting pending owner acceptance.

## Contributing and security

See [CONTRIBUTING.md](CONTRIBUTING.md), [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md),
Expand Down
140 changes: 140 additions & 0 deletions docs/RATELDESK-CONNECTOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# RatelDesk incident connector

The connector sends an incident to a saved, tenant-owned RatelDesk destination.
Delivery requires the authenticated `rateldesk.incident-create.v1` receiver,
its registered NetRatel producer namespace, and the exact approved organization
and requester customer. Saving settings or completing OAuth setup alone does
not enable delivery. Release interoperability evidence is recorded separately.

## Setup

1. Open `/flows/connectors` and choose a tenant for which you have current
connector-management access. Load approved connections and producer. This
displays the existing persisted Flow producer and the installation identity
separately; their GUIDs may differ.
2. For reciprocal connections, approve the existing Flow producer before
incident pairing. Adoption also requires current instance integration
management and an interactive human account. It cannot replace an existing
different producer or change a previous attempt. Review reciprocal
connections through the shared connection panel and select an approved
incident connection for the same tenant.
3. Alternatively, select registered API credential mode. A RatelDesk human
administrator registers the displayed Flow producer against the dedicated
API-purpose credential and the explicit organization/customer. Store its
`rdk_` bearer through the protected credential field. It is never an OAuth
client secret. A replacement API credential needs an approved binding to the
existing RatelDesk namespace; revocation of the predecessor is separate.
4. Save the exact API base, including any configured path base, and mapping.
Reciprocal selection supplies its approved base and organization/customer;
assignment, categories, and severity-to-priority remain explicit.
5. Run the authenticated receiver check. It reads capabilities and validates
the exact source and target without creating an incident. A failed or
interrupted new check invalidates the preceding readiness observation. The
catalog also rechecks current local owner, source, profile and deployment
policy and treats observations older than two minutes as unavailable.

The connection supplies managed credentials from the existing approved profile.
Rotate or revoke them in reciprocal connection management. API credential
rotation updates only the protected credential revision; neither mode copies
secrets into a Flow graph, prepared action, receipt, browser DTO, or audit.
Changing mode, peer, mapping or other semantics advances the connector revision
and cannot retarget queued work. A mode change discards the prior manual secret.

## Deployment restrictions and transport bounds

Optional `RatelDesk:AllowedOrigins` retains exact root HTTPS origin restrictions.
`RatelDesk:AllowedApiBases` additionally restricts exact API/path bases.
`RestrictToConfiguredPeers=true` denies traffic when both restrictions are empty.
Otherwise an empty optional restriction permits the current durable,
owner-approved connector or approved link, rather than discovery alone.
Manual private HTTP requires `RatelDesk:AllowPrivateHttp`; managed mode follows
existing service-link/service-identity private HTTP policy. Browser input and
remote metadata cannot change those permissions or TLS trust.

```json
{
"RatelDesk": {
"AllowedOrigins": ["https://helpdesk.example"],
"AllowedApiBases": ["https://helpdesk.example/support"],
"RestrictToConfiguredPeers": true
}
}
```

Every operation validates the captured API identity and fixed route. DNS and
all resolved IPv4/IPv6 addresses are checked before each new owning socket;
policy is rechecked after DNS and connection. Redirects, proxies, cookies,
automatic transport retries and socket pooling are disabled. TLS retains its
normal hostname and certificate validation. Bounds remain ten seconds per
HTTP operation within the original twenty-second Flow execution budget,
32 KiB requests, 128 KiB responses, eight global/two tenant/one connector
operations, and bounded 1–300 second Retry-After scheduling. No HTTP occurs
inside a Flow database transaction.

## Durable delivery and recovery

Read-only preparation captures receiver/source/namespace, semantic connection
and mapping revisions, fixed endpoints, receiver guarantees, exact wire body,
receiver fingerprint, and wire key. Credentials are freshly resolved for this
captured target on each operation; secret-only rotation cannot change the
snapshot. Target validation is not required to read an already accepted
receipt after category deactivation or assignee changes.

The historical Flow local key remains unchanged. New wire keys use a
SHA-256 domain-separated projection of source, occurrence, event, immutable
Flow version and node GUIDs. Previously durable conforming wire keys remain
exact. A new key is never used to escape uncertainty or fingerprint conflict.
Frozen receiver fingerprint normalization is separate from the preserved V1
Flow semantic fingerprint. V1 prepared records and their immutable hashes are
not rewritten; new receiver evidence is an additive V2 sidecar saved atomically
with initial preparation under the existing lease fence.

Before HTTP, the existing Flow/Monitoring/current-authority guards admit the
operation and persist `MayHaveCommitted`. A verified 201 create, 200 replay or
200 lookup must match the complete immutable receipt, top-level IDs, captured
namespace/key/fingerprint/map and relative Location. Missing, malformed,
redirected, oversized, canceled or transport-failed responses cannot erase a
possible remote commit. Recovered work looks up the same key first. Only a
verified safe same-key/body replay with current authority and the original
horizon can POST again. A 404 lookup does not prove that an in-flight commit
cannot succeed; a 410 never permits a new creation.

At most five POST attempts are admitted. An ambiguous fifth attempt can receive
one durably consumed read-only reconciliation turn, without a sixth POST or a
new action identity. Original run budgets and the 24-hour automatic age survive
restart. Exhaustion with an unconfirmed send remains `DeliveryUnknown`, and
uncertain history is retained rather than pruned as ordinary failure. Receipt
persistence rechecks current tenant authority and the exact lease; a stale
worker cannot finish another worker's action. Flow disablement or suppression
still prevents a new POST while an authorized read-only lookup may recover an
original receipt. External navigation remains absent until a real Web route is
verified; the accepted API Location and original receipt are retained privately.

## Tenant API and Web behavior

Under `/api/v2/tenants/{tenantId}/connectors/rateldesk`:

- `GET` and `GET /{id}` return current configuration/readiness summaries.
- `GET /setup` resolves the existing Flow producer and eligible tenant links.
- `POST /setup/source` takes only `expectedIdentityRevision` and adopts that
existing producer through the foundation human-administrator operation.
- `PUT /{id}` saves configuration and explicit authentication reference using
`expectedRevision`; zero creates a new connector.
- `POST /{id}/credential` rotates only API-purpose credentials using
`expectedCredentialRevision`; managed rotation uses connection management.
- `POST /{id}/connection-test` performs authenticated capability/target reads.
- `POST /{id}/dry-run` returns an inert sanitized preview and sends no incident.

Every route requires current connector-management permission for the exact
tenant before reading configuration. Adoption additionally requires interactive
account and instance integration-management authority. Dispatch retains the
original configuring principal/credential and current SecretUse checks; service
transport authority never supplies a human user FK. Cookie-backed mutations
retain `X-NetRatel-Account-Request: 1`; responses use no-store.

The Web client retains its 30-second operation and streamed 1 MiB response bound.
Dirty drafts, pending-write guards, stable new connector identity, late-response
fences, tenant changes and password clearing remain. The 256-record tenant
admission uses its existing transaction lock and optimistic row CAS. The UI
shows connection status and receiver delivery readiness separately; no hidden
test-incident action is exposed.
20 changes: 20 additions & 0 deletions docs/contracts/rateldesk-receiver-proposal.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"schema": "netratel.rateldesk.receiver.proposal.v1",
"status": "proposal-only-not-implemented-or-negotiated",
"reviewedReceiverCommit": "9a9a8abd2578dc31d1ec500dd3893478f8fa2749",
"normalCreateEndpoint": "/api/v1/incidents/",
"requiresSeparateReceiverScope": true,
"requiredGuarantees": [
"Authenticated API-purpose credential plus current organization/customer authorization before receipt disclosure",
"Authorized source-instance namespace plus stable action key and canonical semantic payload fingerprint",
"Same source/key/fingerprint returns original incident ID and tracking ID",
"Same source/key with different semantic fingerprint fails without another side effect",
"Incident, receipt and deterministic confirmation outbox effect commit atomically",
"Concurrent replay, response loss after commit and process restart preserve one incident and one confirmation enqueue",
"A read-only versioned capability and target-validation response explicitly establishes these guarantees",
"Authenticated receipt reconciliation handles an ambiguous result without changing the action key"
],
"productionCapabilityEndpoint": null,
"productionIdempotencyHeaders": null,
"automaticDeliveryEnabled": false
}
5 changes: 5 additions & 0 deletions src/NetRatel/NetRatel.API.IntegrationTests/ApiFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ async ValueTask IAsyncDisposable.DisposeAsync()
}
}

// Physical tests copy only ready disposable configuration into private startup
// files. Returning this dictionary neither starts an API host nor seeds runtime authority.
internal IReadOnlyDictionary<string, string?> PhysicalRuntimeSettings() =>
_settings.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);

public ApiFactory CreateRuntimeSibling(IReadOnlyDictionary<string, string?> overrides)
{
ArgumentNullException.ThrowIfNull(overrides);
Expand Down
Loading
Loading