chore: initialize go-sip repository

This commit is contained in:
2026-09-21 08:56:04 +08:00
commit 643b11b21f
309 changed files with 40521 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
package contract
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"time"
"unicode/utf8"
"git.ipao.vip/rogee/go-sip/contracts"
"github.com/santhosh-tekuri/jsonschema/v6"
)
type CommandEnvelope struct {
SchemaVersion string `json:"schema_version"`
CommandType string `json:"command_type"`
CommandID string `json:"command_id"`
TenantID string `json:"tenant_id"`
TenantKey string `json:"tenant_key"`
TraceID string `json:"trace_id"`
IssuedAt string `json:"issued_at"`
NotAfter string `json:"not_after"`
Payload json.RawMessage `json:"payload"`
}
type ExecutePayload struct {
ExecutionID string `json:"execution_id"`
TaskID string `json:"task_id"`
TaskItemID string `json:"task_item_id"`
TaskRevision int64 `json:"task_revision"`
Callee string `json:"callee"`
RoutePolicyID string `json:"route_policy_id"`
CallerProfileID string `json:"caller_profile_id"`
AgentVersionID string `json:"agent_version_id"`
Variables map[string]any `json:"variables"`
RingTimeoutMS int64 `json:"ring_timeout_ms"`
MaxCallDurationMS int64 `json:"max_call_duration_ms"`
}
type EventEnvelope struct {
SchemaVersion string `json:"schema_version"`
EventID string `json:"event_id"`
EventType string `json:"event_type"`
TenantID string `json:"tenant_id"`
TenantKey string `json:"tenant_key"`
TraceID string `json:"trace_id"`
OccurredAt string `json:"occurred_at"`
AggregateType string `json:"aggregate_type"`
AggregateID string `json:"aggregate_id"`
AggregateVersion int64 `json:"aggregate_version"`
Payload map[string]any `json:"payload"`
}
var ErrInvalidTenantKey = errors.New("invalid tenant_key")
// ValidateJSON applies the imported JSON Schema. It intentionally validates
// the source contract instead of maintaining a second hand-written schema.
func ValidateJSON(raw []byte) error {
return ValidateSourceSchema("mq.schema.json", raw)
}
// ValidateEvent applies the event-specific payload contract in addition to the
// generic MQ envelope contract.
func ValidateEvent(raw []byte) error {
return ValidateSourceSchema("event-payloads.schema.json", raw)
}
func ValidateSourceSchema(schemaName string, raw []byte) error {
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return fmt.Errorf("decode json: %w", err)
}
var schemaDoc any
if err := contracts.ReadJSON(schemaName, &schemaDoc); err != nil {
return err
}
resource := "https://agent-call.invalid/contracts/" + schemaName
compiler := jsonschema.NewCompiler()
if err := compiler.AddResource(resource, schemaDoc); err != nil {
return fmt.Errorf("register %s: %w", schemaName, err)
}
schema, err := compiler.Compile(resource)
if err != nil {
return fmt.Errorf("compile %s: %w", schemaName, err)
}
if err := schema.Validate(value); err != nil {
return fmt.Errorf("%s validation: %w", schemaName, err)
}
return nil
}
func DecodeExecute(raw []byte) (CommandEnvelope, ExecutePayload, error) {
if err := ValidateJSON(raw); err != nil {
return CommandEnvelope{}, ExecutePayload{}, err
}
var envelope CommandEnvelope
if err := json.Unmarshal(raw, &envelope); err != nil {
return CommandEnvelope{}, ExecutePayload{}, fmt.Errorf("decode command envelope: %w", err)
}
if envelope.CommandType != "call.execute" {
return CommandEnvelope{}, ExecutePayload{}, fmt.Errorf("unsupported command_type %q", envelope.CommandType)
}
if err := ValidateTenantKey(envelope.TenantKey); err != nil {
return CommandEnvelope{}, ExecutePayload{}, err
}
var payload ExecutePayload
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
return CommandEnvelope{}, ExecutePayload{}, fmt.Errorf("decode call.execute payload: %w", err)
}
return envelope, payload, nil
}
func ValidateTenantKey(key string) error {
if key == "" || !utf8.ValidString(key) {
return fmt.Errorf("%w: must be non-empty valid UTF-8", ErrInvalidTenantKey)
}
if len([]byte(key)) > 224 {
return fmt.Errorf("%w: %d UTF-8 bytes exceeds 224-byte routing budget", ErrInvalidTenantKey, len([]byte(key)))
}
return nil
}
func NotAfterExpired(raw string, now time.Time) (bool, error) {
deadline, err := time.Parse(time.RFC3339Nano, raw)
if err != nil {
return false, fmt.Errorf("parse not_after: %w", err)
}
return !now.Before(deadline), nil
}
func CloneJSON(raw []byte) json.RawMessage {
return bytes.Clone(raw)
}
type EventBuilder struct {
TenantID string
TenantKey string
TraceID string
EventType string
Aggregate string
AggregateID string
Version int64
Payload map[string]any
}
func (b EventBuilder) Marshal(now time.Time, eventID string) ([]byte, error) {
if err := ValidateTenantKey(b.TenantKey); err != nil {
return nil, err
}
if b.Version < 1 {
return nil, errors.New("aggregate version must be positive")
}
e := EventEnvelope{
SchemaVersion: "1.0", EventID: eventID, EventType: b.EventType,
TenantID: b.TenantID, TenantKey: b.TenantKey, TraceID: b.TraceID,
OccurredAt: now.UTC().Format(time.RFC3339Nano), AggregateType: b.Aggregate,
AggregateID: b.AggregateID, AggregateVersion: b.Version, Payload: b.Payload,
}
raw, err := json.Marshal(e)
if err != nil {
return nil, err
}
if err := ValidateJSON(raw); err != nil {
return nil, err
}
if err := ValidateEvent(raw); err != nil {
return nil, err
}
return raw, nil
}
+130
View File
@@ -0,0 +1,130 @@
package contract
import (
"strings"
"testing"
"time"
"git.ipao.vip/rogee/go-sip/contracts"
)
func TestDecodeExecuteValidatesImportedSchema(t *testing.T) {
raw, err := contracts.Read("examples/call.execute.json")
if err != nil {
t.Fatal(err)
}
envelope, payload, err := DecodeExecute(raw)
if err != nil {
t.Fatal(err)
}
if envelope.TenantKey != "tenant-demo-key" || payload.ExecutionID == "" {
t.Fatalf("unexpected decoded command: %+v %+v", envelope, payload)
}
}
func TestValidateTenantKeyPreservesRawValueAndBudget(t *testing.T) {
for _, key := range []string{"客户-A", " tenant /raw "} {
if err := ValidateTenantKey(key); err != nil {
t.Fatalf("tenant key %q: %v", key, err)
}
}
if err := ValidateTenantKey(strings.Repeat("x", 225)); err == nil {
t.Fatal("expected routing budget rejection")
}
}
func TestEventFixtures(t *testing.T) {
positive := []string{
"examples/event-command-result.json",
"examples/event-call-status.json",
"examples/event-transcript-updated.json",
"examples/event-call-finished.json",
"examples/event-recording-ready.json",
"examples/event-recording-failed.json",
"examples/event-transcript-failed.json",
"examples/event-contact-opt-out.json",
}
for _, name := range positive {
raw, err := contracts.Read(name)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if err := ValidateEvent(raw); err != nil {
t.Fatalf("%s: %v", name, err)
}
}
invalid, err := contracts.Read("examples/invalid-event-unknown-type.json")
if err != nil {
t.Fatal(err)
}
if err := ValidateEvent(invalid); err == nil {
t.Fatal("expected invalid transcript alias to be rejected")
}
}
func TestProjectOwnedW01Fixtures(t *testing.T) {
valid := []string{
"examples/ai-authorization.json",
"examples/oss-upload-grant.json",
"examples/static-cell-artifact.json",
"p1-development-profile.json",
}
for _, name := range valid {
raw, err := contracts.Read(name)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if err := ValidateSourceSchema(schemaNameForFixture(name), raw); err != nil {
t.Fatalf("%s: %v", name, err)
}
}
invalid := []struct {
name string
schema string
}{
{"examples/invalid-ai-authorization-revoked.json", "ai-authorization.schema.json"},
{"examples/invalid-oss-upload-http.json", "oss-upload.schema.json"},
}
for _, tc := range invalid {
raw, err := contracts.Read(tc.name)
if err != nil {
t.Fatalf("%s: %v", tc.name, err)
}
if err := ValidateSourceSchema(tc.schema, raw); err == nil {
t.Fatalf("%s: expected rejection", tc.name)
}
}
}
func schemaNameForFixture(name string) string {
switch name {
case "examples/ai-authorization.json":
return "ai-authorization.schema.json"
case "examples/oss-upload-grant.json":
return "oss-upload.schema.json"
case "examples/static-cell-artifact.json":
return "static-cell-artifact.schema.json"
case "p1-development-profile.json":
return "p1-development-profile.schema.json"
default:
return ""
}
}
func TestEventBuilder(t *testing.T) {
raw, err := (EventBuilder{
TenantID: "tenant-1", TenantKey: "tenant-1", TraceID: "trace-1",
EventType: "command.result", Aggregate: "command", AggregateID: "cmd-1", Version: 1,
Payload: map[string]any{
"command_id": "cmd-1", "command_type": "call.execute",
"status": "accepted", "reason_code": "accepted",
"execution_id": "execution-1",
},
}).Marshal(time.Unix(0, 0), "event-1")
if err != nil {
t.Fatal(err)
}
if err := ValidateEvent(raw); err != nil {
t.Fatal(err)
}
}
+24
View File
@@ -0,0 +1,24 @@
package contract
import (
"testing"
"git.ipao.vip/rogee/go-sip/contracts"
)
func TestContractBundleIsReadable(t *testing.T) {
for _, name := range []string{
"mq.schema.json", "event-payloads.schema.json", "ai-config.schema.json",
"ai-authorization.schema.json", "oss-upload.schema.json",
"static-cell-artifact.schema.json", "p1-development-profile.schema.json",
"executor.openapi.yaml", "saas.openapi.yaml", "cell-agent.openapi.yaml",
} {
data, err := contracts.Read(name)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if len(data) == 0 {
t.Fatalf("%s is empty", name)
}
}
}
+158
View File
@@ -0,0 +1,158 @@
package contract
import (
"encoding/json"
"fmt"
)
// StaticCellArtifact is the management-approved, immutable Cell/SIP hand-off
// artifact. Its JSON shape is owned by static-cell-artifact.schema.json; this
// type only provides a typed boundary after schema validation.
type StaticCellArtifact struct {
ArtifactID string `json:"artifact_id"`
SourceRelease string `json:"source_release"`
SourceDigest string `json:"source_digest"`
ApprovalReference string `json:"approval_reference"`
CellID string `json:"cell_id"`
Revision uint64 `json:"revision"`
ConfigSHA256 string `json:"config_sha256"`
Mode string `json:"mode"`
AllowedTargets []string `json:"allowed_targets"`
Trunks []StaticTrunk `json:"trunks"`
ARI *StaticARI `json:"ari,omitempty"`
MediaProfiles map[string]StaticMediaProfile `json:"media_profiles,omitempty"`
Media *StaticMedia `json:"media,omitempty"`
Recording *StaticRecording `json:"recording,omitempty"`
LoadEvidence *StaticLoadEvidence `json:"load_evidence"`
}
type StaticTrunk struct {
TrunkID string `json:"trunk_id"`
ProviderID string `json:"provider_id"`
EgressPoolID string `json:"egress_pool_id"`
Codec string `json:"codec"`
CallerProfileIDs []string `json:"caller_profile_ids"`
DialPrefix string `json:"dial_prefix"`
Enabled bool `json:"enabled"`
SIPEndpointRef string `json:"sip_endpoint_ref"`
CredentialRef *string `json:"credential_ref"`
MediaProfileID string `json:"media_profile_id,omitempty"`
}
type StaticARI struct {
BaseURL string `json:"base_url"`
WebsocketURL string `json:"websocket_url"`
Application string `json:"application"`
CredentialRef string `json:"credential_ref"`
}
type StaticMedia struct {
BindAddress string `json:"bind_address"`
Port int `json:"port"`
Format string `json:"format"`
SampleRateHz int `json:"sample_rate_hz"`
Channels int `json:"channels"`
PayloadType int `json:"payload_type"`
}
type StaticMediaProfile struct {
Format string `json:"format"`
SampleRateHz int `json:"sample_rate_hz"`
Channels int `json:"channels"`
PayloadType int `json:"payload_type"`
}
type StaticRecording struct {
Enabled bool `json:"enabled"`
Format string `json:"format"`
Directory string `json:"directory"`
MaxBytes int64 `json:"max_bytes"`
}
type StaticLoadEvidence struct {
AsteriskConfigSHA256 string `json:"asterisk_config_sha256"`
LoadedAt string `json:"loaded_at"`
Status string `json:"status"`
}
// StaticArtifactExpectation contains deployment-local binding constraints.
// Empty string/slice values leave the corresponding optional check disabled;
// the source contract remains mandatory and is always validated first.
type StaticArtifactExpectation struct {
CellID string
Mode string
SourceRelease string
SourceDigest string
ConfigSHA256 string
MinimumRevision uint64
AllowedEgressPoolIDs []string
RequiredTrunkIDs []string
}
// ValidateStaticArtifact validates the imported artifact schema and then
// applies the local Cell hand-off bindings. It deliberately does not claim
// that Asterisk has loaded the artifact: load_evidence.status is explicitly
// "not-yet-loaded" in the contract until an independent load check exists.
func ValidateStaticArtifact(raw []byte, expected StaticArtifactExpectation) (StaticCellArtifact, error) {
if err := ValidateSourceSchema("static-cell-artifact.schema.json", raw); err != nil {
return StaticCellArtifact{}, err
}
var artifact StaticCellArtifact
if err := json.Unmarshal(raw, &artifact); err != nil {
return StaticCellArtifact{}, fmt.Errorf("decode static Cell artifact: %w", err)
}
if expected.CellID != "" && artifact.CellID != expected.CellID {
return StaticCellArtifact{}, fmt.Errorf("static artifact cell binding mismatch: got %q, want %q", artifact.CellID, expected.CellID)
}
if expected.Mode != "" && artifact.Mode != expected.Mode {
return StaticCellArtifact{}, fmt.Errorf("static artifact mode mismatch: got %q, want %q", artifact.Mode, expected.Mode)
}
if expected.SourceRelease != "" && artifact.SourceRelease != expected.SourceRelease {
return StaticCellArtifact{}, fmt.Errorf("static artifact source release mismatch: got %q, want %q", artifact.SourceRelease, expected.SourceRelease)
}
if expected.SourceDigest != "" && artifact.SourceDigest != expected.SourceDigest {
return StaticCellArtifact{}, fmt.Errorf("static artifact source digest mismatch: got %q, want %q", artifact.SourceDigest, expected.SourceDigest)
}
if expected.ConfigSHA256 != "" && artifact.ConfigSHA256 != expected.ConfigSHA256 {
return StaticCellArtifact{}, fmt.Errorf("static artifact config digest mismatch: got %q, want %q", artifact.ConfigSHA256, expected.ConfigSHA256)
}
if expected.MinimumRevision != 0 && artifact.Revision < expected.MinimumRevision {
return StaticCellArtifact{}, fmt.Errorf("static artifact revision %d is older than required %d", artifact.Revision, expected.MinimumRevision)
}
allowedEgress := make(map[string]struct{}, len(expected.AllowedEgressPoolIDs))
for _, egressPoolID := range expected.AllowedEgressPoolIDs {
allowedEgress[egressPoolID] = struct{}{}
}
requiredTrunks := make(map[string]struct{}, len(expected.RequiredTrunkIDs))
for _, trunkID := range expected.RequiredTrunkIDs {
requiredTrunks[trunkID] = struct{}{}
}
seenTrunks := make(map[string]struct{}, len(artifact.Trunks))
for _, trunk := range artifact.Trunks {
if _, duplicate := seenTrunks[trunk.TrunkID]; duplicate {
return StaticCellArtifact{}, fmt.Errorf("static artifact contains duplicate trunk_id %q", trunk.TrunkID)
}
seenTrunks[trunk.TrunkID] = struct{}{}
if len(allowedEgress) != 0 {
if _, ok := allowedEgress[trunk.EgressPoolID]; !ok {
return StaticCellArtifact{}, fmt.Errorf("static artifact trunk %q uses disallowed egress pool %q", trunk.TrunkID, trunk.EgressPoolID)
}
}
if _, required := requiredTrunks[trunk.TrunkID]; required && !trunk.Enabled {
return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is disabled", trunk.TrunkID)
}
if trunk.MediaProfileID != "" {
if _, ok := artifact.MediaProfiles[trunk.MediaProfileID]; !ok {
return StaticCellArtifact{}, fmt.Errorf("static artifact trunk %q references unknown media profile %q", trunk.TrunkID, trunk.MediaProfileID)
}
}
}
for trunkID := range requiredTrunks {
if _, present := seenTrunks[trunkID]; !present {
return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is missing", trunkID)
}
}
return artifact, nil
}
+139
View File
@@ -0,0 +1,139 @@
package contract
import (
"encoding/json"
"testing"
"git.ipao.vip/rogee/go-sip/contracts"
)
func TestValidateStaticArtifactBindsCellAndTrunks(t *testing.T) {
raw, err := contracts.Read("examples/static-cell-artifact.json")
if err != nil {
t.Fatal(err)
}
artifact, err := ValidateStaticArtifact(raw, StaticArtifactExpectation{
CellID: "cell-a",
Mode: "mock",
SourceRelease: "management-snapshot-1",
SourceDigest: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ConfigSHA256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
MinimumRevision: 1,
AllowedEgressPoolIDs: []string{"egress-mock"},
RequiredTrunkIDs: []string{"trunk-mock"},
})
if err != nil {
t.Fatalf("valid artifact rejected: %v", err)
}
if artifact.CellID != "cell-a" || artifact.Revision != 1 || len(artifact.Trunks) != 1 {
t.Fatalf("unexpected artifact: %+v", artifact)
}
}
func TestValidateRealStaticArtifact(t *testing.T) {
raw, err := contracts.Read("examples/static-cell-artifact-real-v1.json")
if err != nil {
t.Fatal(err)
}
artifact, err := ValidateStaticArtifact(raw, StaticArtifactExpectation{
CellID: "cell-single",
Mode: "real",
SourceRelease: "asterisk-22.10.1-native-v1",
SourceDigest: "68006a1a8efed288be4ca4a2ae3cb9554a31d733eac08eaacf4c646c95faf74d",
ConfigSHA256: "89d2686d0d1ca60159c3c6bd725dc9e6f511cbdb56bf6ce7b65ca7d4dc3f2d60",
AllowedEgressPoolIDs: []string{"egress-single"},
RequiredTrunkIDs: []string{"provider-second"},
})
if err != nil {
t.Fatalf("valid real artifact rejected: %v", err)
}
if artifact.ARI == nil || artifact.Media == nil || artifact.Recording == nil {
t.Fatalf("real artifact lost runtime sections: %+v", artifact)
}
if artifact.Media.Format != "alaw" || artifact.Media.SampleRateHz != 8000 || artifact.Media.PayloadType != 8 || artifact.Recording.Format != "wav" {
t.Fatalf("unexpected real media/recording contract: %+v %+v", artifact.Media, artifact.Recording)
}
}
func TestValidateStaticArtifactRejectsBindingViolations(t *testing.T) {
raw, err := contracts.Read("examples/static-cell-artifact.json")
if err != nil {
t.Fatal(err)
}
base := func() StaticArtifactExpectation {
return StaticArtifactExpectation{
CellID: "cell-a",
Mode: "mock",
SourceRelease: "management-snapshot-1",
SourceDigest: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ConfigSHA256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
AllowedEgressPoolIDs: []string{"egress-mock"},
}
}
tests := []struct {
name string
expected StaticArtifactExpectation
mutate func(*StaticCellArtifact)
}{
{name: "wrong cell", expected: func() StaticArtifactExpectation { e := base(); e.CellID = "cell-b"; return e }()},
{name: "wrong source digest", expected: func() StaticArtifactExpectation {
e := base()
e.SourceDigest = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
return e
}()},
{name: "old revision", expected: func() StaticArtifactExpectation { e := base(); e.MinimumRevision = 2; return e }()},
{name: "disallowed egress", expected: func() StaticArtifactExpectation {
e := base()
e.AllowedEgressPoolIDs = []string{"egress-other"}
return e
}()},
{name: "missing required trunk", expected: func() StaticArtifactExpectation {
e := base()
e.RequiredTrunkIDs = []string{"trunk-required"}
return e
}()},
{name: "disabled required trunk", expected: func() StaticArtifactExpectation { e := base(); e.RequiredTrunkIDs = []string{"trunk-mock"}; return e }(), mutate: func(a *StaticCellArtifact) { a.Trunks[0].Enabled = false }},
{name: "duplicate trunk", expected: base(), mutate: func(a *StaticCellArtifact) { a.Trunks = append(a.Trunks, a.Trunks[0]) }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
candidate := raw
if test.mutate != nil {
var artifact StaticCellArtifact
if err := json.Unmarshal(raw, &artifact); err != nil {
t.Fatal(err)
}
test.mutate(&artifact)
candidate, err = json.Marshal(artifact)
if err != nil {
t.Fatal(err)
}
}
if _, err := ValidateStaticArtifact(candidate, test.expected); err == nil {
t.Fatal("expected static artifact validation to fail")
}
})
}
}
func TestValidateStaticArtifactAlwaysChecksSourceSchema(t *testing.T) {
raw, err := contracts.Read("examples/static-cell-artifact.json")
if err != nil {
t.Fatal(err)
}
var value map[string]any
if err := json.Unmarshal(raw, &value); err != nil {
t.Fatal(err)
}
value["unexpected"] = true
candidate, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
if _, err := ValidateStaticArtifact(candidate, StaticArtifactExpectation{}); err == nil {
t.Fatal("expected schema validation failure")
}
}