Skip to content

Commit 8a08a10

Browse files
committed
Implement provisioned credentials
Add model.use and provisioned_credentials wire support, runtime credential lifecycle SPI, revocation stores, client credential access, TCK coverage, docs, and example. Closes #15
1 parent 6d31e95 commit 8a08a10

42 files changed

Lines changed: 1663 additions & 14 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONFORMANCE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ works" is decomposed into multiple binary rows.
8181
| §9.6 `BigDecimal` arithmetic via Jackson `USE_BIG_DECIMAL_FOR_FLOATS` | Implemented | [ArcpMapper.create](arcp-core/src/main/java/dev/arcp/core/wire/ArcpMapper.java) |
8282
| §9.6 Negative metric values rejected | Implemented | [BudgetCounters.decrement](arcp-runtime/src/main/java/dev/arcp/runtime/lease/BudgetCounters.java) returns without decrementing |
8383
| §9.6 `BUDGET_EXHAUSTED` surfaced before authorize-bearing op | Implemented | [JobContext.authorize](arcp-runtime/src/main/java/dev/arcp/runtime/agent/JobContext.java) + `BudgetCounters.ensureAllPositive` |
84+
| §9.7 `model.use` enforcement | Implemented | [LeaseGuard.authorizeModel](arcp-runtime/src/main/java/dev/arcp/runtime/lease/LeaseGuard.java) delegates to generic lease authorization |
85+
| §9.7 `model.use` lease subsetting | Implemented | [Lease.contains](arcp-core/src/main/java/dev/arcp/core/lease/Lease.java) covers narrowed model patterns |
86+
| §9.8 provisioned credentials wire shape | Implemented | [Credential.java](arcp-core/src/main/java/dev/arcp/core/credentials/Credential.java) + [JobAccepted.java](arcp-core/src/main/java/dev/arcp/core/messages/JobAccepted.java) |
87+
| §9.8 credential lifecycle (issue/revoke/rotate) | Implemented | [CredentialProvisioner.java](arcp-runtime/src/main/java/dev/arcp/runtime/credentials/CredentialProvisioner.java) + [CredentialBinding.java](arcp-runtime/src/main/java/dev/arcp/runtime/credentials/CredentialBinding.java) |
8488

8589
## §10. Delegation
8690

@@ -118,6 +122,7 @@ works" is decomposed into multiple binary rows.
118122
| lease-expires-at | Implemented | [examples/lease-expires-at/](examples/lease-expires-at/) |
119123
| idempotent-retry | Implemented | [examples/idempotent-retry/](examples/idempotent-retry/) |
120124
| custom-auth | Implemented | [examples/custom-auth/](examples/custom-auth/) |
125+
| provisioned-credentials | Implemented | [examples/provisioned-credentials/](examples/provisioned-credentials/) |
121126

122127
## §14. Security Considerations
123128

@@ -126,6 +131,8 @@ works" is decomposed into multiple binary rows.
126131
| §14 Subscribe scope (no cross-principal leak) | Implemented | [SessionLoop.handleSubscribe](arcp-runtime/src/main/java/dev/arcp/runtime/session/SessionLoop.java) |
127132
| §14 Budget bypass protection | Implemented | [BudgetCounters.ensureAllPositive](arcp-runtime/src/main/java/dev/arcp/runtime/lease/BudgetCounters.java) gates every authorize call |
128133
| §14 Lease clock check (no past `expires_at`) | Implemented | [LeaseGuard.authorize](arcp-runtime/src/main/java/dev/arcp/runtime/lease/LeaseGuard.java) |
134+
| §14 Credential confidentiality | Implemented | [Credential.toString](arcp-core/src/main/java/dev/arcp/core/credentials/Credential.java) redacts `value`; [JobSummary](arcp-core/src/main/java/dev/arcp/core/messages/JobSummary.java) omits credentials |
135+
| §14 Credential revocation reliability | Implemented | [FileCredentialRevocationStore](arcp-runtime/src/main/java/dev/arcp/runtime/credentials/FileCredentialRevocationStore.java) records outstanding IDs; [ArcpRuntime.Builder](arcp-runtime/src/main/java/dev/arcp/runtime/ArcpRuntime.java) requires a configured revocation store when advertising credentials |
129136
| §14 Host-header / Origin allowlist on WS upgrade | Implemented | [ArcpJakartaAdapter](arcp-middleware-jakarta/src/main/java/dev/arcp/middleware/jakarta/ArcpJakartaAdapter.java) Builder.allowedHosts / allowedOrigins; [ArcpVertxHandler](arcp-middleware-vertx/src/main/java/dev/arcp/middleware/vertx/ArcpVertxHandler.java) Builder.allowedHosts; Spring adapter at the [ArcpSpringBootProperties](arcp-middleware-spring-boot/src/main/java/dev/arcp/middleware/spring/ArcpSpringBootProperties.java) level |
130137

131138
## Out-of-scope for 1.0.0

arcp-client/src/main/java/dev/arcp/client/JobHandle.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package dev.arcp.client;
22

3+
import dev.arcp.core.credentials.Credential;
34
import dev.arcp.core.events.EventBody;
5+
import dev.arcp.core.error.ArcpException;
46
import dev.arcp.core.ids.JobId;
57
import dev.arcp.core.messages.JobAccepted;
68
import dev.arcp.core.messages.JobResult;
7-
import dev.arcp.core.error.ArcpException;
9+
import java.util.List;
10+
import java.util.Optional;
811
import java.util.concurrent.CompletableFuture;
912
import java.util.concurrent.Flow;
1013

@@ -17,6 +20,10 @@ public interface JobHandle {
1720

1821
JobAccepted accepted();
1922

23+
default Optional<List<Credential>> credentials() {
24+
return Optional.ofNullable(accepted().credentials());
25+
}
26+
2027
/** Hot publisher of {@link EventBody} for this job's {@code job.event} stream. */
2128
Flow.Publisher<EventBody> events();
2229

arcp-core/src/main/java/dev/arcp/core/capabilities/Feature.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public enum Feature {
1212
SUBSCRIBE("subscribe"),
1313
LEASE_EXPIRES_AT("lease_expires_at"),
1414
COST_BUDGET("cost.budget"),
15+
MODEL_USE("model.use"),
16+
PROVISIONED_CREDENTIALS("provisioned_credentials"),
1517
PROGRESS("progress"),
1618
RESULT_CHUNK("result_chunk"),
1719
AGENT_VERSIONS("agent_versions");
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dev.arcp.core.credentials;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonInclude;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import java.util.Objects;
7+
import org.jspecify.annotations.Nullable;
8+
9+
@JsonInclude(JsonInclude.Include.NON_NULL)
10+
public record Credential(
11+
CredentialId id,
12+
CredentialScheme scheme,
13+
String value,
14+
String endpoint,
15+
@Nullable String profile,
16+
@Nullable CredentialConstraints constraints) {
17+
@JsonCreator
18+
public Credential(
19+
@JsonProperty("id") CredentialId id,
20+
@JsonProperty("scheme") CredentialScheme scheme,
21+
@JsonProperty("value") String value,
22+
@JsonProperty("endpoint") String endpoint,
23+
@JsonProperty("profile") @Nullable String profile,
24+
@JsonProperty("constraints") @Nullable CredentialConstraints constraints) {
25+
this.id = Objects.requireNonNull(id, "id");
26+
this.scheme = Objects.requireNonNull(scheme, "scheme");
27+
this.value = Objects.requireNonNull(value, "value");
28+
this.endpoint = Objects.requireNonNull(endpoint, "endpoint");
29+
this.profile = profile;
30+
this.constraints = constraints;
31+
}
32+
33+
/** §14: never log or expose the bearer secret through object rendering. */
34+
@Override
35+
public String toString() {
36+
return "Credential[id=" + id + ", scheme=" + scheme + ", endpoint=" + endpoint
37+
+ ", profile=" + profile + ", value=REDACTED]";
38+
}
39+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dev.arcp.core.credentials;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import java.time.Instant;
6+
import java.util.List;
7+
import org.jspecify.annotations.Nullable;
8+
9+
@JsonInclude(JsonInclude.Include.NON_NULL)
10+
public record CredentialConstraints(
11+
@JsonProperty("cost.budget") @Nullable List<String> costBudget,
12+
@JsonProperty("model.use") @Nullable List<String> modelUse,
13+
@JsonProperty("expires_at") @Nullable Instant expiresAt) {
14+
public CredentialConstraints {
15+
costBudget = costBudget == null ? null : List.copyOf(costBudget);
16+
modelUse = modelUse == null ? null : List.copyOf(modelUse);
17+
}
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package dev.arcp.core.credentials;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonValue;
5+
import java.util.Objects;
6+
7+
public record CredentialId(@JsonValue String value) {
8+
public CredentialId {
9+
Objects.requireNonNull(value, "value");
10+
}
11+
12+
public static CredentialId of(String v) {
13+
return new CredentialId(v);
14+
}
15+
16+
@JsonCreator
17+
public static CredentialId fromJson(String v) {
18+
return new CredentialId(v);
19+
}
20+
21+
@Override
22+
public String toString() {
23+
return value;
24+
}
25+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.arcp.core.credentials;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonValue;
5+
import java.util.Arrays;
6+
7+
public enum CredentialScheme {
8+
BEARER("bearer");
9+
10+
private final String wire;
11+
12+
CredentialScheme(String wire) {
13+
this.wire = wire;
14+
}
15+
16+
@JsonValue
17+
public String wire() {
18+
return wire;
19+
}
20+
21+
@JsonCreator
22+
public static CredentialScheme fromWire(String wire) {
23+
return Arrays.stream(values())
24+
.filter(scheme -> scheme.wire.equals(wire))
25+
.findFirst()
26+
.orElseThrow(() -> new IllegalArgumentException(
27+
"unknown credential scheme: " + wire));
28+
}
29+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Wire types for ARCP §9.8 provisioned credentials.
3+
*/
4+
package dev.arcp.core.credentials;

arcp-core/src/main/java/dev/arcp/core/error/BudgetExhaustedException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package dev.arcp.core.error;
22

3-
public final class BudgetExhaustedException extends NonRetryableArcpException {
3+
public class BudgetExhaustedException extends NonRetryableArcpException {
44
public BudgetExhaustedException(String message) {
55
super(ErrorCode.BUDGET_EXHAUSTED, message);
66
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.arcp.core.error;
2+
3+
import org.jspecify.annotations.Nullable;
4+
5+
public final class UpstreamBudgetExhaustedException extends BudgetExhaustedException {
6+
private final @Nullable String upstreamResponseBody;
7+
8+
public UpstreamBudgetExhaustedException(String message, @Nullable String upstreamResponseBody) {
9+
super(message);
10+
this.upstreamResponseBody = upstreamResponseBody;
11+
}
12+
13+
public @Nullable String upstreamResponseBody() {
14+
return upstreamResponseBody;
15+
}
16+
}

0 commit comments

Comments
 (0)