Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e861903
docs(dial-unified-config): rewrite 09-mcp-spec as unified building-bl…
May 5, 2026
10fba3a
docs(dial-unified-config): tighten 09-mcp-spec M4 wording and summary…
May 5, 2026
9ce51d8
docs(dial-unified-config): drop 09-mcp-spec Success Metrics section
May 5, 2026
78acc08
docs(dial-unified-config): pivot 09-mcp-spec to v0.3 in-repo Java module
May 6, 2026
28cd774
docs(dial-unified-config): resolve all 8 MCP-OQs in 09-mcp-spec (v0.4)
May 6, 2026
b4b6feb
mcp spec polishing
May 6, 2026
9198ba0
feat: M.0-pre: bootstrap :mcp Gradle module + /mcp 503 stub
May 6, 2026
3bea196
docs: M.0-pre: backfill commit SHA in §5.6 register
May 6, 2026
2c01d33
feat: M.0.0-bridge: wire Vert.x ↔ MCP-SDK Streamable HTTP transport
May 6, 2026
6da45e3
docs: M.0.0-bridge: backfill commit SHA in §5.6 register
May 6, 2026
9490a8c
feat: M.0.1-pre: lock captured-context bridge and add DialClient loop…
May 6, 2026
5ebaf5b
docs: M.0.1-pre: backfill commit SHA in §5.6 register
May 6, 2026
5cdcfad
feat: M.0.2-pre: per-session rate-limit and concurrency cap for MCP
May 6, 2026
520c89a
docs: M.0.2-pre: backfill commit SHA in §5.6 register
May 6, 2026
614d2b1
feat: M.1.0: read tools bootstrap for DIAL MCP — describe_schema, lis…
May 7, 2026
2a0f3fd
docs: M.1.0: backfill commit SHA in §5.6 register
May 7, 2026
0b10fbc
feat: M.1.1: read-tools entity-type sweep — full 12-type catalog + hi…
May 7, 2026
d494893
feat: M.2.0: bootstrap dial_create/update/delete_resource MCP write t…
May 7, 2026
4304c43
docs: M.2.0: mark slice ✅ and capture write-tools retrospective
May 7, 2026
cc7c813
docs: M.1.1: backfill commit SHA + slice retrospective in §5.6 register
May 7, 2026
0f6e256
feat: M.2.1: extend MCP write tools to settings + files DELETE specials
May 7, 2026
e99ede9
docs: M.2.1: mark slice ✅ and capture write-tools sweep retrospective
May 7, 2026
a16fc11
chore: insert M.3.1-handshake-readiness slice (Track C)
May 7, 2026
3e6f24d
feat: M.3.1-handshake-readiness: defer /mcp dispatch until McpVerticl…
May 7, 2026
b1aca31
docs: M.3.1: mark slice ✅ and capture handshake-readiness retrospective
May 7, 2026
09e76cb
feat: M.3.0: ship dial_upload_file + dial_download_file MCP tools
May 7, 2026
e65f8e1
docs: M.3.0: mark slice ✅ and capture file-tools retrospective
May 7, 2026
1f73761
feat: M.4.0: ship dial_publish_resource MCP tool
May 7, 2026
8556e5e
docs: M.4.0: mark slice ✅ and capture publication-tool retrospective
May 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import lombok.AllArgsConstructor;
import lombok.Data;

import java.net.InetAddress;
import java.net.UnknownHostException;

@AllArgsConstructor
@Data
public class IpAddressRange {
Expand All @@ -21,4 +24,48 @@ public boolean isAddressInRange(byte[] clientIpAddress) {
}
return true;
}

/**
* Parses a CIDR string like {@code 10.0.0.0/8} or {@code fe80::/10} into an
* {@link IpAddressRange}. Used by both the JSON deserializer for client-IP allow-lists
* and by the MCP SSRF guard's CIDR blocklist.
*/
public static IpAddressRange parseCidr(String cidr) {
String[] parts = cidr.trim().split("/");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid CIDR: " + cidr);
}
String base = parts[0].trim();
int prefixLen;
try {
prefixLen = Integer.parseInt(parts[1].trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid CIDR prefix in '" + cidr + "': " + e.getMessage());
}
InetAddress baseAddr;
try {
baseAddr = InetAddress.getByName(base);
} catch (UnknownHostException e) {
throw new IllegalArgumentException("Invalid CIDR base address in '" + cidr + "': " + e.getMessage());
}
byte[] baseBytes = baseAddr.getAddress();

int maxPrefix = baseBytes.length * 8;
if (prefixLen < 0 || prefixLen > maxPrefix) {
throw new IllegalArgumentException("Invalid prefix length " + prefixLen
+ " for " + maxPrefix + "-bit address in '" + cidr + "'");
}
byte[] mask = new byte[baseBytes.length];
int remaining = prefixLen;
for (int i = 0; i < mask.length; i++) {
int bits = Math.min(Math.max(remaining, 0), 8);
mask[i] = (byte) (bits == 0 ? 0 : (0xFF << (8 - bits)) & 0xFF);
remaining -= 8;
}
byte[] maskedBaseIp = new byte[baseBytes.length];
for (int i = 0; i < baseBytes.length; i++) {
maskedBaseIp[i] = (byte) (baseBytes[i] & mask[i] & 0xFF);
}
return new IpAddressRange(mask, maskedBaseIp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import lombok.SneakyThrows;

import java.io.IOException;
import java.net.InetAddress;
import java.util.List;

public class IpAddressRangeDeserializer extends JsonDeserializer<IpAddressRanges> {
Expand All @@ -28,45 +26,8 @@ public IpAddressRanges deserialize(JsonParser jsonParser, DeserializationContext
if (!node.isTextual()) {
throw InvalidFormatException.from(jsonParser, "Expected a JSON string of client IP range", node.toString(), String.class);
}
String cidr = node.textValue();
IpAddressRange range = toIpAddressRange(cidr);
ranges.getRanges().add(range);
ranges.getRanges().add(IpAddressRange.parseCidr(node.textValue()));
}
return ranges;
}

@SneakyThrows
private static IpAddressRange toIpAddressRange(String cidr) {
String[] parts = cidr.trim().split("/");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid CIDR: " + cidr);
}

String base = parts[0].trim();
int prefixLen = Integer.parseInt(parts[1].trim());
InetAddress baseAddr = InetAddress.getByName(base);
byte[] baseBytes = baseAddr.getAddress();

int maxPrefix = baseBytes.length * 8; // 32 for IPv4, 128 for IPv6
if (prefixLen < 0 || prefixLen > maxPrefix) {
throw new IllegalArgumentException("Invalid prefix length " + prefixLen
+ " for " + maxPrefix + "-bit address");
}

byte[] mask = new byte[baseBytes.length];
int remaining = prefixLen;
for (int i = 0; i < mask.length; i++) {
int bits = Math.min(Math.max(remaining, 0), 8);
int maskByte = bits == 0 ? 0 : (0xFF << (8 - bits)) & 0xFF;
mask[i] = (byte) maskByte;
remaining -= 8;
}

byte[] maskedBaseIp = new byte[baseBytes.length];
for (int i = 0; i < baseBytes.length; i++) {
int baseMasked = baseBytes[i] & mask[i] & 0xFF;
maskedBaseIp[i] = (byte) baseMasked;
}
return new IpAddressRange(mask, maskedBaseIp);
}
}
2 changes: 1 addition & 1 deletion docs/sandbox/dial-unified-config/03-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ Listing is per-bucket — `GET /v1/{type}/{bucket}/`. Admin enumerates the relev
}
```

`hasMore` is **always present** (`true` or `false`) on every listing response; `nextCursor` is present **iff** `hasMore: true` and is omitted on the last page. The two fields are kept consistent — the two-field shape is convenient for clients that prefer either explicit `hasMore` checks or `nextCursor`-presence checks. The cursor is opaque and clients must not parse it. The Admin MCP's `dial_admin_list_entities` paginates the underlying listing endpoint (issuing `?limit=500` per page) until `hasMore: false` (for bounded entity types) or until its per-invocation ceiling of 2,500 items (5 pages) for potentially unbounded types (`files`, `prompts`, `conversations`) — see [`09-admin-mcp-spec.md`](09-admin-mcp-spec.md) §6.1 for the full draining and truncation semantics.
`hasMore` is **always present** (`true` or `false`) on every listing response; `nextCursor` is present **iff** `hasMore: true` and is omitted on the last page. The two fields are kept consistent — the two-field shape is convenient for clients that prefer either explicit `hasMore` checks or `nextCursor`-presence checks. The cursor is opaque and clients must not parse it. The Admin MCP's `dial_list_resources` paginates the underlying listing endpoint (issuing `?limit=500` per page) until `hasMore: false` (for bounded entity types) or until its per-invocation ceiling of 2,500 items (5 pages) for potentially unbounded types (`files`, `prompts`, `conversations`) — see [`09-admin-mcp-spec.md`](09-admin-mcp-spec.md) §6.1 for the full draining and truncation semantics.

**`name` field synthesis.** The `name` value on each list item (and on `GET` of a single entity) is **always synthesized** by the controller — for API-managed entities the **full canonical ID** (the `Config` map key, e.g. `models/public/gpt-4`); for file-sourced entities the simple-name `Config` map key (e.g. `gpt-4`). It is never deserialized from the persisted JSON body. **Amendment 2026-05-08 (Polish.1):** prior to this round API-managed entries projected `simpleName(mapKey)` in the listing/GET; canonical IDs were exposed only on the legacy `/openai/...` listings. Operators copy-pasting an API entry's listing row into a per-entity URL needed to reconstruct the canonical prefix manually, and a file-vs-API simple-name collision silently lost one row in the listing. Polish.1 projects the full canonical ID for API entries so the row is copy-paste-friendly and the dedup keying on the full map key (see *Listing dedup* below) preserves both rows on collision. File-sourced entries are unchanged. Implementers wiring the listing controller must populate `name` from the canonical map key for API entries and from the simple map key for file entries — not expect it on the persisted body.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ See `dial_secrets_storage_analysis.md` for the full evaluation of alternative ap

## 3. Audit

> **STATUS: WIP / DEFERRED.** The audit subsystem is **deferred to Phase 7** (Audit & Compliance) — after full entity-management API support, CLI surface, and Admin MCP land. §3 below remains as the working design draft for that future phase. **Phase 1–6 make no commitment to R-Audit-1 or R-Audit-2** and ship without an audit trail. Cross-references from other documents to specific §3 subsections (storage layout, event schema, CLI commands, `/v1/admin/audit`, `dial_admin_query_audit` MCP tool) all carry the same WIP status. See [`07-migration-and-rollout.md`](07-migration-and-rollout.md) Phase 7 for the rollout placement and rationale.
> **STATUS: WIP / DEFERRED.** The audit subsystem is **deferred to Phase 7** (Audit & Compliance) — after full entity-management API support, CLI surface, and Admin MCP land. §3 below remains as the working design draft for that future phase. **Phase 1–6 make no commitment to R-Audit-1 or R-Audit-2** and ship without an audit trail. Cross-references from other documents to specific §3 subsections (storage layout, event schema, CLI commands, `/v1/admin/audit`, `dial_query_audit` MCP tool) all carry the same WIP status. See [`07-migration-and-rollout.md`](07-migration-and-rollout.md) Phase 7 for the rollout placement and rationale.

### 3.1 Requirements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ Phase 5 is a **major Admin Backend refactor**, not a thin adapter swap. The dire
- Storage: Redis Streams (hot) + blob archival (cold) per [`04-security-and-audit.md`](04-security-and-audit.md) §3.4.
- `GET /v1/admin/audit` query API + filters per [`04-security-and-audit.md`](04-security-and-audit.md) §3.5.
- `dial-cli audit` command group (history, log, snapshot, rollback, reconcile) per [`06-cli-user-guide.md`](06-cli-user-guide.md).
- `dial_admin_query_audit` MCP tool per [`09-admin-mcp-spec.md`](09-admin-mcp-spec.md).
- `dial_query_audit` MCP tool per [`09-admin-mcp-spec.md`](09-admin-mcp-spec.md).
- Admin Backend audit-table retirement + Admin UI history-view rewrite (moved here from Phase 5).
- Snapshot / point-in-time reconstruction + boundary-snapshot preservation.
- `PublicationService` audit — to be triaged at Phase-7 planning, may slip to Phase 7.5+.
Expand Down
Loading