Gate public bridge actions on Bifrost identity
This commit is contained in:
parent
c533a7f98c
commit
445c27978b
9 changed files with 325 additions and 0 deletions
|
|
@ -79,6 +79,7 @@ For the full staged roadmap, including contribution points, revenue share, patro
|
||||||
- GitHub bridge actions that claim Bifrost-owned provenance now validate that provenance too: a `bifrost_agent_transport_request` source must point at a known request-lane request for the same repo, and governance-backed bridge actions must point at a known Bifrost topic instead of a decorative source string
|
- GitHub bridge actions that claim Bifrost-owned provenance now validate that provenance too: a `bifrost_agent_transport_request` source must point at a known request-lane request for the same repo, and governance-backed bridge actions must point at a known Bifrost topic instead of a decorative source string
|
||||||
- governance-backed GitHub publication is also no longer allowed to jump the queue: a Bifrost governance topic must have been approved or promoted before it can justify a GitHub bridge action
|
- governance-backed GitHub publication is also no longer allowed to jump the queue: a Bifrost governance topic must have been approved or promoted before it can justify a GitHub bridge action
|
||||||
- non-GitHub bridge activity is no longer allowed to drift off-book either: `discord-post`, `discord-dm`, and `reddit-post` now require `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` by default, with `--allow-unreceipted-activity true` reserved for explicit local recovery and refused inside dispatched work
|
- non-GitHub bridge activity is no longer allowed to drift off-book either: `discord-post`, `discord-dm`, and `reddit-post` now require `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` by default, with `--allow-unreceipted-activity true` reserved for explicit local recovery and refused inside dispatched work
|
||||||
|
- Persona and agent `discord-post` / `reddit-post` bridge actions also require a Bifrost identity plus Heimdall-backed capability/account reference in the hosted ledger; pass `--identity`, `--heimdall-capability-ref`, and the Epiphany provenance flags or set the matching environment variables. The bridge token is an actuator credential, not identity or provider authority.
|
||||||
- Bifrost-dispatched Codex turns also inject Bifrost-owned GitHub gates through environment config: `git` and `gh` wrappers are prepended in `PATH`, a `pre-push` hook backs up raw Git pushes, raw `git push` stays blocked, and direct `gh` usage is limited to explicit read-only commands unless `tools/bifrost-bridge.mjs` authorizes the bridge-owned GitHub write path
|
- Bifrost-dispatched Codex turns also inject Bifrost-owned GitHub gates through environment config: `git` and `gh` wrappers are prepended in `PATH`, a `pre-push` hook backs up raw Git pushes, raw `git push` stays blocked, and direct `gh` usage is limited to explicit read-only commands unless `tools/bifrost-bridge.mjs` authorizes the bridge-owned GitHub write path
|
||||||
- dispatched Codex turns now default to `workspace-write` instead of full machine access, and the app-server sandbox policy disables network access unless the operator explicitly chooses a broader sandbox
|
- dispatched Codex turns now default to `workspace-write` instead of full machine access, and the app-server sandbox policy disables network access unless the operator explicitly chooses a broader sandbox
|
||||||
- `tools/agent-transport.mjs` also uses that bridge configuration to record `/transport/receipts` entries for queue, claim, release, and close events on the request lane
|
- `tools/agent-transport.mjs` also uses that bridge configuration to record `/transport/receipts` entries for queue, claim, release, and close events on the request lane
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,13 @@ The hosted app now exposes a first-class bridge action ledger for governed trans
|
||||||
- `POST /bridge/actions/{id}/fail`
|
- `POST /bridge/actions/{id}/fail`
|
||||||
|
|
||||||
The owner is Bifrost, not the local script. The script or agent may execute the crossing, but Bifrost records the request, policy decision, lifecycle state, and receipt.
|
The owner is Bifrost, not the local script. The script or agent may execute the crossing, but Bifrost records the request, policy decision, lifecycle state, and receipt.
|
||||||
|
Persona and agent actions that cross into Discord or Reddit must carry a Bifrost
|
||||||
|
identity plus a Heimdall-backed capability/account reference at request time.
|
||||||
|
The local bridge token only proves that the actuator may call the ledger; it is
|
||||||
|
not the actor identity and not outside-provider authority. The ledger stores the
|
||||||
|
Bifrost identity, Heimdall reference, Epiphany run id, lane id, and agent
|
||||||
|
identity beside the action so later receipts can answer which agent did what
|
||||||
|
without opening local transcripts or trusting helper-script folklore.
|
||||||
|
|
||||||
The hosted app also exposes a dispatch-run ledger for the older Bifrost-launched worker bridge:
|
The hosted app also exposes a dispatch-run ledger for the older Bifrost-launched worker bridge:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,11 @@ public sealed class BifrostDbContext(DbContextOptions<BifrostDbContext> options)
|
||||||
entity.Property(x => x.SourceKind).HasMaxLength(120);
|
entity.Property(x => x.SourceKind).HasMaxLength(120);
|
||||||
entity.Property(x => x.SourceId).HasMaxLength(240);
|
entity.Property(x => x.SourceId).HasMaxLength(240);
|
||||||
entity.Property(x => x.AuthorityReference).HasMaxLength(240);
|
entity.Property(x => x.AuthorityReference).HasMaxLength(240);
|
||||||
|
entity.Property(x => x.BifrostIdentity).HasMaxLength(160);
|
||||||
|
entity.Property(x => x.HeimdallCapabilityReference).HasMaxLength(240);
|
||||||
|
entity.Property(x => x.EpiphanyRunId).HasMaxLength(160);
|
||||||
|
entity.Property(x => x.EpiphanyLaneId).HasMaxLength(120);
|
||||||
|
entity.Property(x => x.EpiphanyAgentIdentity).HasMaxLength(160);
|
||||||
entity.Property(x => x.PolicyDecision).HasMaxLength(500);
|
entity.Property(x => x.PolicyDecision).HasMaxLength(500);
|
||||||
entity.Property(x => x.Title).HasMaxLength(240);
|
entity.Property(x => x.Title).HasMaxLength(240);
|
||||||
entity.Property(x => x.ReceiptUrl).HasMaxLength(500);
|
entity.Property(x => x.ReceiptUrl).HasMaxLength(500);
|
||||||
|
|
@ -423,6 +428,8 @@ public sealed class BifrostDbContext(DbContextOptions<BifrostDbContext> options)
|
||||||
entity.HasIndex(x => new { x.TargetSurface, x.ActionKind, x.Status });
|
entity.HasIndex(x => new { x.TargetSurface, x.ActionKind, x.Status });
|
||||||
entity.HasIndex(x => new { x.ActorKind, x.ActorName });
|
entity.HasIndex(x => new { x.ActorKind, x.ActorName });
|
||||||
entity.HasIndex(x => new { x.SourceKind, x.SourceId });
|
entity.HasIndex(x => new { x.SourceKind, x.SourceId });
|
||||||
|
entity.HasIndex(x => x.BifrostIdentity);
|
||||||
|
entity.HasIndex(x => x.HeimdallCapabilityReference);
|
||||||
entity.HasOne(x => x.ActorUserAccount)
|
entity.HasOne(x => x.ActorUserAccount)
|
||||||
.WithMany(x => x.BridgeActions)
|
.WithMany(x => x.BridgeActions)
|
||||||
.HasForeignKey(x => x.ActorUserAccountId)
|
.HasForeignKey(x => x.ActorUserAccountId)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bifrost.Web.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBridgeActionIdentityProvenance : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BifrostIdentity",
|
||||||
|
table: "BridgeActions",
|
||||||
|
type: "character varying(160)",
|
||||||
|
maxLength: 160,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "EpiphanyAgentIdentity",
|
||||||
|
table: "BridgeActions",
|
||||||
|
type: "character varying(160)",
|
||||||
|
maxLength: 160,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "EpiphanyLaneId",
|
||||||
|
table: "BridgeActions",
|
||||||
|
type: "character varying(120)",
|
||||||
|
maxLength: 120,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "EpiphanyRunId",
|
||||||
|
table: "BridgeActions",
|
||||||
|
type: "character varying(160)",
|
||||||
|
maxLength: 160,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "HeimdallCapabilityReference",
|
||||||
|
table: "BridgeActions",
|
||||||
|
type: "character varying(240)",
|
||||||
|
maxLength: 240,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BridgeActions_BifrostIdentity",
|
||||||
|
table: "BridgeActions",
|
||||||
|
column: "BifrostIdentity");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BridgeActions_HeimdallCapabilityReference",
|
||||||
|
table: "BridgeActions",
|
||||||
|
column: "HeimdallCapabilityReference");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_BridgeActions_BifrostIdentity",
|
||||||
|
table: "BridgeActions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_BridgeActions_HeimdallCapabilityReference",
|
||||||
|
table: "BridgeActions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BifrostIdentity",
|
||||||
|
table: "BridgeActions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EpiphanyAgentIdentity",
|
||||||
|
table: "BridgeActions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EpiphanyLaneId",
|
||||||
|
table: "BridgeActions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EpiphanyRunId",
|
||||||
|
table: "BridgeActions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "HeimdallCapabilityReference",
|
||||||
|
table: "BridgeActions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -279,9 +279,29 @@ namespace Bifrost.Web.Data.Migrations
|
||||||
.HasMaxLength(240)
|
.HasMaxLength(240)
|
||||||
.HasColumnType("character varying(240)");
|
.HasColumnType("character varying(240)");
|
||||||
|
|
||||||
|
b.Property<string>("BifrostIdentity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("CompletedAtUtc")
|
b.Property<DateTimeOffset?>("CompletedAtUtc")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("EpiphanyAgentIdentity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
|
b.Property<string>("EpiphanyLaneId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<string>("EpiphanyRunId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
b.Property<string>("ExternalReceiptId")
|
b.Property<string>("ExternalReceiptId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(240)
|
.HasMaxLength(240)
|
||||||
|
|
@ -291,6 +311,11 @@ namespace Bifrost.Web.Data.Migrations
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("HeimdallCapabilityReference")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(240)
|
||||||
|
.HasColumnType("character varying(240)");
|
||||||
|
|
||||||
b.Property<Guid?>("MotionId")
|
b.Property<Guid?>("MotionId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
|
@ -361,6 +386,10 @@ namespace Bifrost.Web.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("ActorUserAccountId");
|
b.HasIndex("ActorUserAccountId");
|
||||||
|
|
||||||
|
b.HasIndex("BifrostIdentity");
|
||||||
|
|
||||||
|
b.HasIndex("HeimdallCapabilityReference");
|
||||||
|
|
||||||
b.HasIndex("MotionId");
|
b.HasIndex("MotionId");
|
||||||
|
|
||||||
b.HasIndex("Status");
|
b.HasIndex("Status");
|
||||||
|
|
|
||||||
|
|
@ -926,6 +926,16 @@ public sealed class BridgeAction
|
||||||
|
|
||||||
public string AuthorityReference { get; set; } = string.Empty;
|
public string AuthorityReference { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string BifrostIdentity { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string HeimdallCapabilityReference { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string EpiphanyRunId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string EpiphanyLaneId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string EpiphanyAgentIdentity { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string PolicyDecision { get; set; } = string.Empty;
|
public string PolicyDecision { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ public sealed class BridgeActionService(
|
||||||
SourceKind = NormalizeText(normalizedRequest.SourceKind),
|
SourceKind = NormalizeText(normalizedRequest.SourceKind),
|
||||||
SourceId = NormalizeText(normalizedRequest.SourceId),
|
SourceId = NormalizeText(normalizedRequest.SourceId),
|
||||||
AuthorityReference = NormalizeText(normalizedRequest.AuthorityReference),
|
AuthorityReference = NormalizeText(normalizedRequest.AuthorityReference),
|
||||||
|
BifrostIdentity = NormalizeText(normalizedRequest.BifrostIdentity),
|
||||||
|
HeimdallCapabilityReference = NormalizeText(normalizedRequest.HeimdallCapabilityReference),
|
||||||
|
EpiphanyRunId = NormalizeText(normalizedRequest.EpiphanyRunId),
|
||||||
|
EpiphanyLaneId = NormalizeText(normalizedRequest.EpiphanyLaneId),
|
||||||
|
EpiphanyAgentIdentity = NormalizeText(normalizedRequest.EpiphanyAgentIdentity),
|
||||||
PolicyDecision = policy.Decision,
|
PolicyDecision = policy.Decision,
|
||||||
Title = NormalizeText(normalizedRequest.Title),
|
Title = NormalizeText(normalizedRequest.Title),
|
||||||
Summary = NormalizeText(normalizedRequest.Summary),
|
Summary = NormalizeText(normalizedRequest.Summary),
|
||||||
|
|
@ -200,6 +205,19 @@ public sealed class BridgeActionService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RequiresExternalAccountCapability(request))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.BifrostIdentity))
|
||||||
|
{
|
||||||
|
return BridgePolicyDecision.Reject("Persona and agent Discord/Reddit bridge actions must carry a Bifrost identity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.HeimdallCapabilityReference))
|
||||||
|
{
|
||||||
|
return BridgePolicyDecision.Reject("Persona and agent Discord/Reddit bridge actions must carry a Heimdall-backed capability or account reference.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.WorkItemId is not null)
|
if (request.WorkItemId is not null)
|
||||||
{
|
{
|
||||||
var workItem = await dbContext.WorkItems
|
var workItem = await dbContext.WorkItems
|
||||||
|
|
@ -350,6 +368,10 @@ public sealed class BridgeActionService(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool RequiresExternalAccountCapability(BridgeActionRequest request) =>
|
||||||
|
request.ActorKind is BridgeActorKind.Agent or BridgeActorKind.Persona &&
|
||||||
|
request.TargetSurface is BridgeTargetSurface.Discord or BridgeTargetSurface.Reddit;
|
||||||
|
|
||||||
private static string NormalizeRepository(string value) => NormalizeText(value).ToLowerInvariant();
|
private static string NormalizeRepository(string value) => NormalizeText(value).ToLowerInvariant();
|
||||||
|
|
||||||
private static string ShortRepositoryName(string normalizedRepositoryFullName)
|
private static string ShortRepositoryName(string normalizedRepositoryFullName)
|
||||||
|
|
@ -391,6 +413,11 @@ public sealed record BridgeActionRequest(
|
||||||
string SourceKind,
|
string SourceKind,
|
||||||
string SourceId,
|
string SourceId,
|
||||||
string AuthorityReference,
|
string AuthorityReference,
|
||||||
|
string BifrostIdentity,
|
||||||
|
string HeimdallCapabilityReference,
|
||||||
|
string EpiphanyRunId,
|
||||||
|
string EpiphanyLaneId,
|
||||||
|
string EpiphanyAgentIdentity,
|
||||||
string Title,
|
string Title,
|
||||||
string Summary,
|
string Summary,
|
||||||
Guid? WorkItemId = null,
|
Guid? WorkItemId = null,
|
||||||
|
|
@ -408,6 +435,11 @@ public sealed record BridgeActionRequest(
|
||||||
SourceKind = NormalizeText(SourceKind),
|
SourceKind = NormalizeText(SourceKind),
|
||||||
SourceId = NormalizeText(SourceId),
|
SourceId = NormalizeText(SourceId),
|
||||||
AuthorityReference = NormalizeText(AuthorityReference),
|
AuthorityReference = NormalizeText(AuthorityReference),
|
||||||
|
BifrostIdentity = NormalizeText(BifrostIdentity),
|
||||||
|
HeimdallCapabilityReference = NormalizeText(HeimdallCapabilityReference),
|
||||||
|
EpiphanyRunId = NormalizeText(EpiphanyRunId),
|
||||||
|
EpiphanyLaneId = NormalizeText(EpiphanyLaneId),
|
||||||
|
EpiphanyAgentIdentity = NormalizeText(EpiphanyAgentIdentity),
|
||||||
Title = NormalizeText(Title),
|
Title = NormalizeText(Title),
|
||||||
Summary = NormalizeText(Summary)
|
Summary = NormalizeText(Summary)
|
||||||
};
|
};
|
||||||
|
|
@ -434,6 +466,11 @@ public sealed record BridgeActionResult(
|
||||||
string SourceKind,
|
string SourceKind,
|
||||||
string SourceId,
|
string SourceId,
|
||||||
string AuthorityReference,
|
string AuthorityReference,
|
||||||
|
string BifrostIdentity,
|
||||||
|
string HeimdallCapabilityReference,
|
||||||
|
string EpiphanyRunId,
|
||||||
|
string EpiphanyLaneId,
|
||||||
|
string EpiphanyAgentIdentity,
|
||||||
string PolicyDecision,
|
string PolicyDecision,
|
||||||
string Title,
|
string Title,
|
||||||
string ReceiptUrl,
|
string ReceiptUrl,
|
||||||
|
|
@ -453,6 +490,11 @@ public sealed record BridgeActionResult(
|
||||||
action.SourceKind,
|
action.SourceKind,
|
||||||
action.SourceId,
|
action.SourceId,
|
||||||
action.AuthorityReference,
|
action.AuthorityReference,
|
||||||
|
action.BifrostIdentity,
|
||||||
|
action.HeimdallCapabilityReference,
|
||||||
|
action.EpiphanyRunId,
|
||||||
|
action.EpiphanyLaneId,
|
||||||
|
action.EpiphanyAgentIdentity,
|
||||||
action.PolicyDecision,
|
action.PolicyDecision,
|
||||||
action.Title,
|
action.Title,
|
||||||
action.ReceiptUrl,
|
action.ReceiptUrl,
|
||||||
|
|
|
||||||
|
|
@ -702,6 +702,130 @@ public sealed class PlatformEndpointsTests : IClassFixture<TestWebApplicationFac
|
||||||
Assert.Equal("Authorized", action.Status);
|
Assert.Equal("Authorized", action.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Persona_reddit_action_without_bifrost_identity_or_heimdall_reference_is_denied()
|
||||||
|
{
|
||||||
|
using var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
var requestPayload = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
actorKind = "Persona",
|
||||||
|
actorName = "Epiphany Persona",
|
||||||
|
targetSurface = "Reddit",
|
||||||
|
actionKind = "RedditPost",
|
||||||
|
targetLocator = "r/GameCultOrg",
|
||||||
|
sourceKind = "epiphany_persona_reddit",
|
||||||
|
sourceId = "persona-speech-audit-missing-identity",
|
||||||
|
authorityReference = "epiphany.persona_speech_audit",
|
||||||
|
title = "Persona Reddit thread",
|
||||||
|
summary = "This should not pass without Bifrost identity plus Heimdall reference."
|
||||||
|
});
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
|
||||||
|
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
|
||||||
|
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
|
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
|
||||||
|
Assert.NotNull(action);
|
||||||
|
Assert.Equal("Denied", action.Status);
|
||||||
|
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
|
||||||
|
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
|
||||||
|
|
||||||
|
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
|
||||||
|
Assert.Contains("Bifrost identity", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Persona_discord_action_requires_heimdall_reference_after_bifrost_identity()
|
||||||
|
{
|
||||||
|
using var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
var requestPayload = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
actorKind = "Persona",
|
||||||
|
actorName = "Epiphany Persona",
|
||||||
|
targetSurface = "Discord",
|
||||||
|
actionKind = "DiscordPost",
|
||||||
|
targetLocator = "channel/1501196543150264332",
|
||||||
|
sourceKind = "epiphany_persona_speech",
|
||||||
|
sourceId = "persona-speech-audit-missing-heimdall",
|
||||||
|
authorityReference = "epiphany.persona_speech_audit",
|
||||||
|
bifrostIdentity = "epiphany.Persona",
|
||||||
|
title = "Persona Discord post",
|
||||||
|
summary = "This should not pass without an outside-account capability reference."
|
||||||
|
});
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
|
||||||
|
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
|
||||||
|
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
|
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
|
||||||
|
Assert.NotNull(action);
|
||||||
|
Assert.Equal("Denied", action.Status);
|
||||||
|
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
|
||||||
|
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
|
||||||
|
|
||||||
|
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
|
||||||
|
Assert.Contains("Heimdall", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Persona_reddit_action_with_bifrost_identity_and_heimdall_reference_is_authorized_and_stored()
|
||||||
|
{
|
||||||
|
using var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
var requestPayload = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
actorKind = "Persona",
|
||||||
|
actorName = "Epiphany Persona",
|
||||||
|
targetSurface = "Reddit",
|
||||||
|
actionKind = "RedditPost",
|
||||||
|
targetLocator = "r/GameCultOrg",
|
||||||
|
sourceKind = "epiphany_persona_reddit",
|
||||||
|
sourceId = "persona-speech-audit-authorized",
|
||||||
|
authorityReference = "epiphany.persona_speech_audit",
|
||||||
|
bifrostIdentity = "epiphany.Persona",
|
||||||
|
heimdallCapabilityReference = "heimdall:reddit:capability:epiphany-persona",
|
||||||
|
epiphanyRunId = "epiphany-run-identity-gate",
|
||||||
|
epiphanyLaneId = "Persona",
|
||||||
|
epiphanyAgentIdentity = "epiphany.Persona",
|
||||||
|
title = "Persona Reddit thread",
|
||||||
|
summary = "This should pass with Bifrost identity and Heimdall reference."
|
||||||
|
});
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
|
||||||
|
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
|
||||||
|
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||||
|
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
|
||||||
|
Assert.NotNull(action);
|
||||||
|
Assert.Equal("Authorized", action.Status);
|
||||||
|
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
|
||||||
|
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
|
||||||
|
|
||||||
|
Assert.Equal(BridgeActionStatus.Authorized, savedAction.Status);
|
||||||
|
Assert.Equal("epiphany.Persona", savedAction.BifrostIdentity);
|
||||||
|
Assert.Equal("heimdall:reddit:capability:epiphany-persona", savedAction.HeimdallCapabilityReference);
|
||||||
|
Assert.Equal("epiphany-run-identity-gate", savedAction.EpiphanyRunId);
|
||||||
|
Assert.Equal("Persona", savedAction.EpiphanyLaneId);
|
||||||
|
Assert.Equal("epiphany.Persona", savedAction.EpiphanyAgentIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Local_bridge_token_can_record_dispatch_run_lifecycle()
|
public async Task Local_bridge_token_can_record_dispatch_run_lifecycle()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1026,6 +1026,15 @@ async function beginBridgeAction(options, action) {
|
||||||
sourceKind: optionalString(options["source-kind"]) ?? optionalString(process.env.BIFROST_BRIDGE_SOURCE_KIND) ?? "",
|
sourceKind: optionalString(options["source-kind"]) ?? optionalString(process.env.BIFROST_BRIDGE_SOURCE_KIND) ?? "",
|
||||||
sourceId: optionalString(options["source-id"]) ?? optionalString(process.env.BIFROST_BRIDGE_SOURCE_ID) ?? "",
|
sourceId: optionalString(options["source-id"]) ?? optionalString(process.env.BIFROST_BRIDGE_SOURCE_ID) ?? "",
|
||||||
authorityReference: optionalString(options["authority-ref"]) ?? optionalString(process.env.BIFROST_BRIDGE_AUTHORITY_REF) ?? "",
|
authorityReference: optionalString(options["authority-ref"]) ?? optionalString(process.env.BIFROST_BRIDGE_AUTHORITY_REF) ?? "",
|
||||||
|
bifrostIdentity: optionalString(options.identity) ?? optionalString(process.env.BIFROST_IDENTITY) ?? "",
|
||||||
|
heimdallCapabilityReference:
|
||||||
|
optionalString(options["heimdall-capability-ref"]) ??
|
||||||
|
optionalString(process.env.HEIMDALL_CAPABILITY_REF) ??
|
||||||
|
optionalString(process.env.BIFROST_HEIMDALL_CAPABILITY_REF) ??
|
||||||
|
"",
|
||||||
|
epiphanyRunId: optionalString(options["epiphany-run-id"]) ?? optionalString(process.env.EPIPHANY_RUN_ID) ?? "",
|
||||||
|
epiphanyLaneId: optionalString(options["epiphany-lane-id"]) ?? optionalString(process.env.EPIPHANY_LANE_ID) ?? "",
|
||||||
|
epiphanyAgentIdentity: optionalString(options["epiphany-agent-identity"]) ?? optionalString(process.env.EPIPHANY_AGENT_IDENTITY) ?? "",
|
||||||
workItemId: optionalString(options["work-item-id"]) ?? null,
|
workItemId: optionalString(options["work-item-id"]) ?? null,
|
||||||
motionId: optionalString(options["motion-id"]) ?? null,
|
motionId: optionalString(options["motion-id"]) ?? null,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue